diff --git a/internal/config/config.go b/internal/config/config.go index ac6b600..356e6f1 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -305,6 +305,7 @@ func (e *Element) UnmarshalYAML(unmarshal func(interface{}) error) error { Task string `yaml:"task"` Depends []interface{} `yaml:"depends"` IgnoreFailure bool `yaml:"ignore_failure"` + Approval bool `yaml:"approval"` When *when `yaml:"when"` } @@ -317,6 +318,7 @@ func (e *Element) UnmarshalYAML(unmarshal func(interface{}) error) error { e.Name = te.Name e.Task = te.Task e.IgnoreFailure = te.IgnoreFailure + e.Approval = te.Approval depends := make([]*Depend, len(te.Depends)) for i, dependEntry := range te.Depends { diff --git a/internal/runconfig/runconfig.go b/internal/runconfig/runconfig.go index dd862ba..4d64e24 100644 --- a/internal/runconfig/runconfig.go +++ b/internal/runconfig/runconfig.go @@ -182,6 +182,7 @@ func GenRunConfig(uuid util.UUIDGenerator, c *config.Config, pipelineName string Steps: steps, IgnoreFailure: cpe.IgnoreFailure, Skip: !include, + NeedsApproval: cpe.Approval, } rc.Tasks[t.ID] = t diff --git a/internal/runconfig/runconfig_test.go b/internal/runconfig/runconfig_test.go index 0018538..e087b91 100644 --- a/internal/runconfig/runconfig_test.go +++ b/internal/runconfig/runconfig_test.go @@ -771,7 +771,7 @@ func TestGenRunConfig(t *testing.T) { Tag: &types.WhenConditions{Include: []types.WhenCondition{{Match: "v1.x"}, {Match: "v2.x"}}}, Ref: &types.WhenConditions{ Include: []types.WhenCondition{{Match: "master"}}, - Exclude: []types.WhenCondition{{Match: "/branch01/", Type: types.WhenConditionTypeRegExp}, {Match: "branch02"}}, + Exclude: []types.WhenCondition{{Match: "branch01", Type: types.WhenConditionTypeRegExp}, {Match: "branch02"}}, }, }, }, diff --git a/internal/services/gateway/api/run.go b/internal/services/gateway/api/run.go index 11a331b..5aa783f 100644 --- a/internal/services/gateway/api/run.go +++ b/internal/services/gateway/api/run.go @@ -70,6 +70,10 @@ type RunResponseTask struct { Level int `json:"level"` Depends []*rstypes.RunConfigTaskDepend `json:"depends"` + WaitingApproval bool `json:"waiting_approval"` + Approved bool `json:"approved"` + ApprovalAnnotations map[string]string `json:"approval_annotations"` + StartTime *time.Time `json:"start_time"` EndTime *time.Time `json:"end_time"` } @@ -79,6 +83,10 @@ type RunTaskResponse struct { Name string `json:"name"` Status rstypes.RunTaskStatus `json:"status"` + WaitingApproval bool `json:"waiting_approval"` + Approved bool `json:"approved"` + ApprovalAnnotations map[string]string `json:"approval_annotations"` + SetupStep *RunTaskResponseSetupStep `json:"setup_step"` Steps []*RunTaskResponseStep `json:"steps"` @@ -139,6 +147,10 @@ func createRunResponseTask(r *rstypes.Run, rt *rstypes.RunTask, rct *rstypes.Run StartTime: rt.StartTime, EndTime: rt.EndTime, + WaitingApproval: rt.WaitingApproval, + Approved: rt.Approved, + ApprovalAnnotations: rt.ApprovalAnnotations, + Level: rct.Level, Depends: rct.Depends, } @@ -151,7 +163,12 @@ func createRunTaskResponse(rt *rstypes.RunTask, rct *rstypes.RunConfigTask) *Run ID: rt.ID, Name: rct.Name, Status: rt.Status, - Steps: make([]*RunTaskResponseStep, len(rt.Steps)), + + WaitingApproval: rt.WaitingApproval, + Approved: rt.Approved, + ApprovalAnnotations: rt.ApprovalAnnotations, + + Steps: make([]*RunTaskResponseStep, len(rt.Steps)), StartTime: rt.StartTime, EndTime: rt.EndTime, @@ -390,12 +407,12 @@ func (h *RunActionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { switch req.ActionType { case RunActionTypeRestart: - req := &rsapi.RunCreateRequest{ + rsreq := &rsapi.RunCreateRequest{ RunID: runID, FromStart: req.FromStart, } - resp, err := h.runserviceClient.CreateRun(ctx, req) + resp, err := h.runserviceClient.CreateRun(ctx, rsreq) if err != nil { if resp != nil && resp.StatusCode == http.StatusNotFound { http.Error(w, err.Error(), http.StatusNotFound) @@ -407,11 +424,11 @@ func (h *RunActionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } case RunActionTypeStop: - req := &rsapi.RunActionsRequest{ + rsreq := &rsapi.RunActionsRequest{ ActionType: rsapi.RunActionTypeStop, } - resp, err := h.runserviceClient.RunActions(ctx, runID, req) + resp, err := h.runserviceClient.RunActions(ctx, runID, rsreq) if err != nil { if resp != nil && resp.StatusCode == http.StatusNotFound { http.Error(w, err.Error(), http.StatusNotFound) @@ -424,6 +441,62 @@ func (h *RunActionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } +type RunTaskActionType string + +const ( + RunTaskActionTypeApprove RunTaskActionType = "approve" +) + +type RunTaskActionsRequest struct { + ActionType RunTaskActionType `json:"action_type"` + ApprovalAnnotations map[string]string `json:"approval_annotations,omitempty"` +} + +type RunTaskActionsHandler struct { + log *zap.SugaredLogger + runserviceClient *rsapi.Client +} + +func NewRunTaskActionsHandler(logger *zap.Logger, runserviceClient *rsapi.Client) *RunTaskActionsHandler { + return &RunTaskActionsHandler{log: logger.Sugar(), runserviceClient: runserviceClient} +} + +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 RunTaskActionsRequest + d := json.NewDecoder(r.Body) + if err := d.Decode(&req); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + switch req.ActionType { + case RunTaskActionTypeApprove: + rsreq := &rsapi.RunTaskActionsRequest{ + ActionType: rsapi.RunTaskActionTypeApprove, + ApprovalAnnotations: req.ApprovalAnnotations, + } + + resp, err := h.runserviceClient.RunTaskActions(ctx, runID, taskID, rsreq) + if err != nil { + if resp != nil && resp.StatusCode == http.StatusNotFound { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + h.log.Errorf("err: %+v", err) + httpError(w, err) + return + } + default: + http.Error(w, "", http.StatusBadRequest) + return + } +} + type LogsHandler struct { log *zap.SugaredLogger runserviceClient *rsapi.Client diff --git a/internal/services/gateway/gateway.go b/internal/services/gateway/gateway.go index f4f2361..0da9627 100644 --- a/internal/services/gateway/gateway.go +++ b/internal/services/gateway/gateway.go @@ -191,6 +191,7 @@ func (g *Gateway) Run(ctx context.Context) error { runsHandler := api.NewRunsHandler(logger, g.runserviceClient) runtaskHandler := api.NewRuntaskHandler(logger, g.runserviceClient) runActionsHandler := api.NewRunActionsHandler(logger, g.runserviceClient) + runTaskActionsHandler := api.NewRunTaskActionsHandler(logger, g.runserviceClient) logsHandler := api.NewLogsHandler(logger, g.runserviceClient) @@ -263,6 +264,7 @@ func (g *Gateway) Run(ctx context.Context) error { apirouter.Handle("/runs/{runid}", authForcedHandler(runHandler)).Methods("GET") apirouter.Handle("/runs/{runid}/actions", authForcedHandler(runActionsHandler)).Methods("PUT") apirouter.Handle("/runs/{runid}/tasks/{taskid}", authForcedHandler(runtaskHandler)).Methods("GET") + apirouter.Handle("/runs/{runid}/tasks/{taskid}/actions", runTaskActionsHandler).Methods("PUT") apirouter.Handle("/runs", authForcedHandler(runsHandler)).Methods("GET") router.Handle("/login", loginUserHandler).Methods("POST") diff --git a/internal/services/runservice/scheduler/api/api.go b/internal/services/runservice/scheduler/api/api.go index 96deaaf..994cca7 100644 --- a/internal/services/runservice/scheduler/api/api.go +++ b/internal/services/runservice/scheduler/api/api.go @@ -585,7 +585,6 @@ func (h *RunTaskActionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request runID := vars["runid"] taskID := vars["taskid"] - // TODO(sgotti) Check authorized call from client var req RunTaskActionsRequest d := json.NewDecoder(r.Body) if err := d.Decode(&req); err != nil { diff --git a/internal/services/runservice/scheduler/command/command.go b/internal/services/runservice/scheduler/command/command.go index 8fb86d1..aaea957 100644 --- a/internal/services/runservice/scheduler/command/command.go +++ b/internal/services/runservice/scheduler/command/command.go @@ -421,10 +421,11 @@ func (s *CommandHandler) ApproveRunTask(ctx context.Context, req *RunTaskApprove return errors.Errorf("run %q, task %q is not in waiting approval state", r.ID, req.TaskID) } - if !task.Approved { + if task.Approved { return errors.Errorf("run %q, task %q is already approved", r.ID, req.TaskID) } + task.WaitingApproval = false task.Approved = true task.ApprovalAnnotations = req.ApprovalAnnotations diff --git a/internal/services/runservice/scheduler/scheduler.go b/internal/services/runservice/scheduler/scheduler.go index b0ea382..e0c2c13 100644 --- a/internal/services/runservice/scheduler/scheduler.go +++ b/internal/services/runservice/scheduler/scheduler.go @@ -117,9 +117,12 @@ func (s *Scheduler) advanceRunTasks(ctx context.Context, r *types.Run) error { } if canRun { - if !rt.WaitingApproval && rct.NeedsApproval { + // now that the task can run set it to waiting approval if needed + if rct.NeedsApproval && !rt.WaitingApproval && !rt.Approved { rt.WaitingApproval = true - } else { + } + // Run only if approved if needed + if !rct.NeedsApproval || (rct.NeedsApproval && rt.Approved) { tasksToRun = append(tasksToRun, rt) } }