mirror of
https://github.com/goharbor/harbor.git
synced 2024-12-03 15:43:39 +01:00
provide api to show log of scan job
This commit is contained in:
parent
b127ba391d
commit
ea25c3cfe5
@ -16,6 +16,7 @@ package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/vmware/harbor/src/common/dao"
|
||||
"github.com/vmware/harbor/src/common/models"
|
||||
@ -83,3 +84,17 @@ func (isj *ImageScanJob) Post() {
|
||||
log.Debugf("Sent job to scheduler, job: %v", 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/actions", &api.ReplicationJob{}, "post:HandleAction")
|
||||
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
|
||||
}
|
||||
|
||||
const (
|
||||
//ReplicationJobType ...
|
||||
ReplicationJobType = "replication"
|
||||
//ScanJobType ...
|
||||
ScanJobType = "scan"
|
||||
)
|
||||
|
||||
// Prepare inits security context and project manager from request
|
||||
// context
|
||||
func (b *BaseController) Prepare() {
|
||||
|
@ -16,8 +16,6 @@ package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
@ -147,38 +145,12 @@ func (ra *RepJobAPI) GetLog() {
|
||||
if ra.jobID == 0 {
|
||||
ra.CustomAbort(http.StatusBadRequest, "id is nil")
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", buildJobLogURL(strconv.FormatInt(ra.jobID, 10)), nil)
|
||||
url := buildJobLogURL(strconv.FormatInt(ra.jobID, 10), ReplicationJobType)
|
||||
err := utils.RequestAsUI(http.MethodGet, url, nil, utils.NewJobLogRespHandler(&ra.BaseAPI))
|
||||
if err != nil {
|
||||
log.Errorf("failed to create a request: %v", err)
|
||||
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))
|
||||
}
|
||||
ra.RenderError(http.StatusInternalServerError, err.Error())
|
||||
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
|
||||
|
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 (this *ScanJobAPI) Prepare() {
|
||||
this.BaseController.Prepare()
|
||||
if !this.SecurityCtx.IsAuthenticated() {
|
||||
this.HandleUnauthorized()
|
||||
return
|
||||
}
|
||||
id, err := this.GetInt64FromPath(":id")
|
||||
if err != nil {
|
||||
this.CustomAbort(http.StatusBadRequest, "ID is invalid")
|
||||
}
|
||||
this.jobID = id
|
||||
|
||||
data, err := dao.GetScanJob(id)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to load job data for job: %d, error: %v", id, err)
|
||||
this.CustomAbort(http.StatusInternalServerError, "Failed to get Job data")
|
||||
}
|
||||
projectName := strings.SplitN(data.Repository, "/", 2)[0]
|
||||
if !this.SecurityCtx.HasReadPerm(projectName) {
|
||||
log.Errorf("User does not have read permission for project: %s", projectName)
|
||||
this.HandleForbidden(this.SecurityCtx.GetUsername())
|
||||
}
|
||||
this.projectName = projectName
|
||||
}
|
||||
|
||||
//GetLog ...
|
||||
func (this *ScanJobAPI) GetLog() {
|
||||
url := buildJobLogURL(strconv.FormatInt(this.jobID, 10), ScanJobType)
|
||||
err := utils.RequestAsUI(http.MethodGet, url, nil, utils.NewJobLogRespHandler(&this.BaseAPI))
|
||||
if err != nil {
|
||||
this.RenderError(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
@ -97,7 +97,7 @@ func TriggerReplication(policyID int64, repository string,
|
||||
}
|
||||
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
|
||||
@ -406,9 +406,9 @@ func buildReplicationURL() string {
|
||||
return fmt.Sprintf("%s/api/jobs/replication", url)
|
||||
}
|
||||
|
||||
func buildJobLogURL(jobID string) string {
|
||||
func buildJobLogURL(jobID string, jobType string) string {
|
||||
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 {
|
||||
|
@ -99,6 +99,7 @@ func initRouters() {
|
||||
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]+)/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", &api.RepPolicyAPI{}, "get:List")
|
||||
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"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
@ -74,8 +73,7 @@ func ScanAllImages() error {
|
||||
|
||||
// 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
|
||||
// TODO: add a response handler to replace expectSC *when needed*
|
||||
func RequestAsUI(method, url string, body io.Reader, expectSC int) error {
|
||||
func RequestAsUI(method, url string, body io.Reader, h ResponseHandler) error {
|
||||
req, err := http.NewRequest(method, url, body)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -87,16 +85,7 @@ func RequestAsUI(method, url string, body io.Reader, expectSC int) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
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
|
||||
return h.Handle(resp)
|
||||
}
|
||||
|
||||
//AddUISecret add secret cookie to a request
|
||||
@ -120,7 +109,7 @@ func TriggerImageScan(repository string, tag string) error {
|
||||
return err
|
||||
}
|
||||
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 ...
|
||||
|
Loading…
Reference in New Issue
Block a user