agola/internal/services/gateway/api/run.go

565 lines
13 KiB
Go

// Copyright 2019 Sorint.lab
//
// 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 (
"encoding/json"
"io"
"net/http"
"strconv"
"agola.io/agola/internal/errors"
"agola.io/agola/internal/services/gateway/action"
"agola.io/agola/internal/util"
gwapitypes "agola.io/agola/services/gateway/api/types"
rstypes "agola.io/agola/services/runservice/types"
"github.com/gorilla/mux"
"github.com/rs/zerolog"
)
func createRunResponse(r *rstypes.Run, rc *rstypes.RunConfig) *gwapitypes.RunResponse {
run := &gwapitypes.RunResponse{
ID: r.ID,
Counter: r.Counter,
Name: r.Name,
Annotations: r.Annotations,
Phase: r.Phase,
Result: r.Result,
Stopping: r.Stop,
SetupErrors: rc.SetupErrors,
Tasks: make(map[string]*gwapitypes.RunResponseTask),
TasksWaitingApproval: r.TasksWaitingApproval(),
EnqueueTime: r.EnqueueTime,
StartTime: r.StartTime,
EndTime: r.EndTime,
}
run.CanRestartFromScratch, _ = r.CanRestartFromScratch()
run.CanRestartFromFailedTasks, _ = r.CanRestartFromFailedTasks()
for name, rt := range r.Tasks {
rct := rc.Tasks[rt.ID]
run.Tasks[name] = createRunResponseTask(r, rt, rct)
}
return run
}
func createRunResponseTask(r *rstypes.Run, rt *rstypes.RunTask, rct *rstypes.RunConfigTask) *gwapitypes.RunResponseTask {
t := &gwapitypes.RunResponseTask{
ID: rt.ID,
Name: rct.Name,
Status: rt.Status,
StartTime: rt.StartTime,
EndTime: rt.EndTime,
WaitingApproval: rt.WaitingApproval,
Approved: rt.Approved,
ApprovalAnnotations: rt.Annotations,
Level: rct.Level,
Depends: rct.Depends,
}
return t
}
func createRunTaskResponse(rt *rstypes.RunTask, rct *rstypes.RunConfigTask) *gwapitypes.RunTaskResponse {
t := &gwapitypes.RunTaskResponse{
ID: rt.ID,
Name: rct.Name,
Status: rt.Status,
WaitingApproval: rt.WaitingApproval,
Approved: rt.Approved,
ApprovalAnnotations: rt.Annotations,
Steps: make([]*gwapitypes.RunTaskResponseStep, len(rt.Steps)),
StartTime: rt.StartTime,
EndTime: rt.EndTime,
}
t.SetupStep = &gwapitypes.RunTaskResponseSetupStep{
Name: "Task setup",
Phase: rt.SetupStep.Phase,
StartTime: rt.SetupStep.StartTime,
EndTime: rt.SetupStep.EndTime,
}
for i := 0; i < len(t.Steps); i++ {
s := &gwapitypes.RunTaskResponseStep{
Phase: rt.Steps[i].Phase,
StartTime: rt.Steps[i].StartTime,
EndTime: rt.Steps[i].EndTime,
}
rcts := rct.Steps[i]
rts := rt.Steps[i]
if rts.LogPhase == rstypes.RunTaskFetchPhaseFinished {
s.LogArchived = true
}
switch rcts := rcts.(type) {
case *rstypes.RunStep:
s.Type = "run"
s.Name = rcts.Name
s.Command = rcts.Command
shell := rcts.Shell
if shell == "" {
shell = rct.Shell
}
s.Shell = shell
s.ExitStatus = rts.ExitStatus
case *rstypes.SaveToWorkspaceStep:
s.Type = "save_to_workspace"
s.Name = "save to workspace"
case *rstypes.RestoreWorkspaceStep:
s.Type = "restore_workspace"
s.Name = "restore workspace"
case *rstypes.SaveCacheStep:
s.Type = "save_cache"
s.Name = "save cache"
case *rstypes.RestoreCacheStep:
s.Type = "restore_cache"
s.Name = "restore cache"
}
t.Steps[i] = s
}
return t
}
type RunHandler struct {
log zerolog.Logger
ah *action.ActionHandler
}
func NewRunHandler(log zerolog.Logger, ah *action.ActionHandler) *RunHandler {
return &RunHandler{log: log, ah: ah}
}
func (h *RunHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vars := mux.Vars(r)
runID := vars["runid"]
runResp, err := h.ah.GetRun(ctx, runID)
if util.HTTPError(w, err) {
h.log.Err(err).Send()
return
}
res := createRunResponse(runResp.Run, runResp.RunConfig)
if err := util.HTTPResponse(w, http.StatusOK, res); err != nil {
h.log.Err(err).Send()
}
}
type RuntaskHandler struct {
log zerolog.Logger
ah *action.ActionHandler
}
func NewRuntaskHandler(log zerolog.Logger, ah *action.ActionHandler) *RuntaskHandler {
return &RuntaskHandler{log: log, ah: ah}
}
func (h *RuntaskHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vars := mux.Vars(r)
runID := vars["runid"]
taskID := vars["taskid"]
runResp, err := h.ah.GetRun(ctx, runID)
if util.HTTPError(w, err) {
h.log.Err(err).Send()
return
}
run := runResp.Run
rc := runResp.RunConfig
rt, ok := run.Tasks[taskID]
if !ok {
util.HTTPError(w, util.NewAPIError(util.ErrNotExist, errors.Errorf("run %q task %q not found", runID, taskID)))
return
}
rct := rc.Tasks[rt.ID]
res := createRunTaskResponse(rt, rct)
if err := util.HTTPResponse(w, http.StatusOK, res); err != nil {
h.log.Err(err).Send()
}
}
const (
DefaultRunsLimit = 25
MaxRunsLimit = 40
)
func createRunsResponse(r *rstypes.Run) *gwapitypes.RunsResponse {
run := &gwapitypes.RunsResponse{
ID: r.ID,
Counter: r.Counter,
Name: r.Name,
Annotations: r.Annotations,
Phase: r.Phase,
Result: r.Result,
TasksWaitingApproval: r.TasksWaitingApproval(),
EnqueueTime: r.EnqueueTime,
StartTime: r.StartTime,
EndTime: r.EndTime,
}
return run
}
type RunsHandler struct {
log zerolog.Logger
ah *action.ActionHandler
}
func NewRunsHandler(log zerolog.Logger, ah *action.ActionHandler) *RunsHandler {
return &RunsHandler{log: log, ah: ah}
}
func (h *RunsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
q := r.URL.Query()
// we currently accept only one group
group := q.Get("group")
// we require that groups are specified to not return all runs
if group == "" {
util.HTTPError(w, util.NewAPIError(util.ErrBadRequest, errors.Errorf("no groups specified")))
return
}
phaseFilter := q["phase"]
resultFilter := q["result"]
changeGroups := q["changegroup"]
_, lastRun := q["lastrun"]
limitS := q.Get("limit")
limit := DefaultRunsLimit
if limitS != "" {
var err error
limit, err = strconv.Atoi(limitS)
if err != nil {
util.HTTPError(w, util.NewAPIError(util.ErrBadRequest, errors.Wrapf(err, "cannot parse limit")))
return
}
}
if limit < 0 {
util.HTTPError(w, util.NewAPIError(util.ErrBadRequest, errors.Errorf("limit must be greater or equal than 0")))
return
}
if limit > MaxRunsLimit {
limit = MaxRunsLimit
}
asc := false
if _, ok := q["asc"]; ok {
asc = true
}
start := q.Get("start")
areq := &action.GetRunsRequest{
PhaseFilter: phaseFilter,
ResultFilter: resultFilter,
Group: group,
LastRun: lastRun,
ChangeGroups: changeGroups,
StartRunID: start,
Limit: limit,
Asc: asc,
}
runsResp, err := h.ah.GetRuns(ctx, areq)
if util.HTTPError(w, err) {
h.log.Err(err).Send()
return
}
runs := make([]*gwapitypes.RunsResponse, len(runsResp.Runs))
for i, r := range runsResp.Runs {
runs[i] = createRunsResponse(r)
}
if err := util.HTTPResponse(w, http.StatusOK, runs); err != nil {
h.log.Err(err).Send()
}
}
type RunActionsHandler struct {
log zerolog.Logger
ah *action.ActionHandler
}
func NewRunActionsHandler(log zerolog.Logger, ah *action.ActionHandler) *RunActionsHandler {
return &RunActionsHandler{log: log, ah: ah}
}
func (h *RunActionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vars := mux.Vars(r)
runID := vars["runid"]
var req gwapitypes.RunActionsRequest
d := json.NewDecoder(r.Body)
if err := d.Decode(&req); err != nil {
util.HTTPError(w, util.NewAPIError(util.ErrBadRequest, err))
return
}
areq := &action.RunActionsRequest{
RunID: runID,
ActionType: action.RunActionType(req.ActionType),
FromStart: req.FromStart,
}
runResp, err := h.ah.RunAction(ctx, areq)
if util.HTTPError(w, err) {
h.log.Err(err).Send()
return
}
res := createRunResponse(runResp.Run, runResp.RunConfig)
if err := util.HTTPResponse(w, http.StatusOK, res); err != nil {
h.log.Err(err).Send()
}
}
type RunTaskActionsHandler struct {
log zerolog.Logger
ah *action.ActionHandler
}
func NewRunTaskActionsHandler(log zerolog.Logger, ah *action.ActionHandler) *RunTaskActionsHandler {
return &RunTaskActionsHandler{log: log, ah: ah}
}
func (h *RunTaskActionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
vars := mux.Vars(r)
runID := vars["runid"]
taskID := vars["taskid"]
var req gwapitypes.RunTaskActionsRequest
d := json.NewDecoder(r.Body)
if err := d.Decode(&req); err != nil {
util.HTTPError(w, util.NewAPIError(util.ErrBadRequest, err))
return
}
areq := &action.RunTaskActionsRequest{
RunID: runID,
TaskID: taskID,
ActionType: action.RunTaskActionType(req.ActionType),
}
err := h.ah.RunTaskAction(ctx, areq)
if util.HTTPError(w, err) {
h.log.Err(err).Send()
return
}
}
type LogsHandler struct {
log zerolog.Logger
ah *action.ActionHandler
}
func NewLogsHandler(log zerolog.Logger, ah *action.ActionHandler) *LogsHandler {
return &LogsHandler{log: log, ah: ah}
}
func (h *LogsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
q := r.URL.Query()
runID := q.Get("runID")
if runID == "" {
util.HTTPError(w, util.NewAPIError(util.ErrBadRequest, errors.Errorf("empty run id")))
return
}
taskID := q.Get("taskID")
if taskID == "" {
util.HTTPError(w, util.NewAPIError(util.ErrBadRequest, errors.Errorf("empty task id")))
return
}
_, setup := q["setup"]
stepStr := q.Get("step")
if !setup && stepStr == "" {
util.HTTPError(w, util.NewAPIError(util.ErrBadRequest, errors.Errorf("no setup or step number provided")))
return
}
if setup && stepStr != "" {
util.HTTPError(w, util.NewAPIError(util.ErrBadRequest, errors.Errorf("both setup and step number provided")))
return
}
var step int
if stepStr != "" {
var err error
step, err = strconv.Atoi(stepStr)
if err != nil {
util.HTTPError(w, util.NewAPIError(util.ErrBadRequest, errors.Wrapf(err, "cannot parse step number")))
return
}
}
follow := false
if _, ok := q["follow"]; ok {
follow = true
}
areq := &action.GetLogsRequest{
RunID: runID,
TaskID: taskID,
Setup: setup,
Step: step,
Follow: follow,
}
resp, err := h.ah.GetLogs(ctx, areq)
if util.HTTPError(w, err) {
h.log.Err(err).Send()
return
}
// write and flush the headers so the client will receive the response
// header also if there're currently no lines to send
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.WriteHeader(http.StatusOK)
var flusher http.Flusher
if fl, ok := w.(http.Flusher); ok {
flusher = fl
}
if flusher != nil {
flusher.Flush()
}
defer resp.Body.Close()
if err := sendLogs(w, resp.Body); err != nil {
h.log.Err(err).Send()
return
}
}
// sendLogs streams received logs lines and flushes them
func sendLogs(w io.Writer, r io.Reader) error {
buf := make([]byte, 4096)
var flusher http.Flusher
if fl, ok := w.(http.Flusher); ok {
flusher = fl
}
stop := false
for {
if stop {
return nil
}
n, err := r.Read(buf)
if err != nil {
if err != io.EOF {
return errors.WithStack(err)
}
if n == 0 {
return nil
}
stop = true
}
if _, err := w.Write(buf[:n]); err != nil {
return errors.WithStack(err)
}
if flusher != nil {
flusher.Flush()
}
}
}
type LogsDeleteHandler struct {
log zerolog.Logger
ah *action.ActionHandler
}
func NewLogsDeleteHandler(log zerolog.Logger, ah *action.ActionHandler) *LogsDeleteHandler {
return &LogsDeleteHandler{log: log, ah: ah}
}
func (h *LogsDeleteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
q := r.URL.Query()
runID := q.Get("runID")
if runID == "" {
util.HTTPError(w, util.NewAPIError(util.ErrBadRequest, errors.Errorf("empty run id")))
return
}
taskID := q.Get("taskID")
if taskID == "" {
util.HTTPError(w, util.NewAPIError(util.ErrBadRequest, errors.Errorf("empty task id")))
return
}
_, setup := q["setup"]
stepStr := q.Get("step")
if !setup && stepStr == "" {
util.HTTPError(w, util.NewAPIError(util.ErrBadRequest, errors.Errorf("no setup or step number provided")))
return
}
if setup && stepStr != "" {
util.HTTPError(w, util.NewAPIError(util.ErrBadRequest, errors.Errorf("both setup and step number provided")))
return
}
var step int
if stepStr != "" {
var err error
step, err = strconv.Atoi(stepStr)
if err != nil {
util.HTTPError(w, util.NewAPIError(util.ErrBadRequest, errors.Wrapf(err, "cannot parse step number")))
return
}
}
areq := &action.DeleteLogsRequest{
RunID: runID,
TaskID: taskID,
Setup: setup,
Step: step,
}
err := h.ah.DeleteLogs(ctx, areq)
if util.HTTPError(w, err) {
h.log.Err(err).Send()
return
}
}