Merge pull request #2830 from reasonerjt/swagger-update-1.2.0

provide api to show log of scan job
This commit is contained in:
Daniel Jiang 2017-07-21 14:19:58 +08:00 committed by GitHub
commit c68514bfb7
10 changed files with 231 additions and 48 deletions

View File

@ -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)
}

View File

@ -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")
} }

View File

@ -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() {

View File

@ -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
View 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
}
}

View File

@ -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 {

View File

@ -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")

View 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,
}
}

View 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")
}

View File

@ -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 ...