116 lines
3.5 KiB
Go
116 lines
3.5 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
|
|
"felis.lolicon.best/internal/nodecontrol"
|
|
)
|
|
|
|
type NodeControl interface {
|
|
List(context.Context) ([]nodecontrol.Task, error)
|
|
Get(context.Context, string) (nodecontrol.Task, error)
|
|
Start(context.Context, nodecontrol.Request, string) (nodecontrol.Task, error)
|
|
}
|
|
|
|
func (a *API) handleNodeTasks(w http.ResponseWriter, r *http.Request) {
|
|
if a.NodeControl == nil {
|
|
writeJSON(w, 200, map[string]any{"available": false, "tasks": []nodecontrol.Task{}})
|
|
return
|
|
}
|
|
tasks, err := a.NodeControl.List(r.Context())
|
|
if err != nil {
|
|
a.writeNodeControlError(w, r, err)
|
|
return
|
|
}
|
|
writeJSON(w, 200, map[string]any{"available": true, "tasks": tasks})
|
|
}
|
|
func (a *API) handleNodeTask(w http.ResponseWriter, r *http.Request) {
|
|
if !a.nodeControlReady(w, r) {
|
|
return
|
|
}
|
|
task, err := a.NodeControl.Get(r.Context(), r.PathValue("id"))
|
|
if err != nil {
|
|
a.writeNodeControlError(w, r, err)
|
|
return
|
|
}
|
|
writeJSON(w, 200, task)
|
|
}
|
|
func (a *API) handleStartNodeTask(w http.ResponseWriter, r *http.Request) {
|
|
if !a.requireReauth(w, r, principalFromContext(r.Context())) || !a.nodeControlReady(w, r) {
|
|
return
|
|
}
|
|
if err := requireJSONContentType(r); err != nil {
|
|
writeError(w, r, err)
|
|
return
|
|
}
|
|
var req nodecontrol.Request
|
|
if err := decodeJSON(w, r, &req); err != nil {
|
|
writeError(w, r, err)
|
|
return
|
|
}
|
|
a.startNodeTask(w, r, req)
|
|
}
|
|
func (a *API) handleRetryNodeTask(w http.ResponseWriter, r *http.Request) {
|
|
if !a.requireReauth(w, r, principalFromContext(r.Context())) || !a.nodeControlReady(w, r) {
|
|
return
|
|
}
|
|
task, err := a.NodeControl.Get(r.Context(), r.PathValue("id"))
|
|
if err != nil {
|
|
a.writeNodeControlError(w, r, err)
|
|
return
|
|
}
|
|
if task.State != "failed" {
|
|
writeError(w, r, newError(409, "conflict", "only failed node tasks can be retried"))
|
|
return
|
|
}
|
|
a.startNodeTask(w, r, task.Request)
|
|
}
|
|
func (a *API) startNodeTask(w http.ResponseWriter, r *http.Request, req nodecontrol.Request) {
|
|
if err := req.Validate(); err != nil {
|
|
writeError(w, r, newError(400, "bad_request", "%v", err))
|
|
return
|
|
}
|
|
task, err := a.NodeControl.Start(r.Context(), req, principalFromContext(r.Context()).UserID)
|
|
if err != nil {
|
|
a.writeNodeControlError(w, r, err)
|
|
return
|
|
}
|
|
a.audit(r, "platform.node."+req.Action, task.ID)
|
|
writeJSON(w, http.StatusAccepted, task)
|
|
}
|
|
func (a *API) nodeControlReady(w http.ResponseWriter, r *http.Request) bool {
|
|
if a.NodeControl == nil {
|
|
a.writeNodeControlError(w, r, errors.New("node-control not configured"))
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
func (a *API) writeNodeControlError(w http.ResponseWriter, r *http.Request, err error) {
|
|
switch {
|
|
case errors.Is(err, nodecontrol.ErrBusy):
|
|
writeError(w, r, newError(409, "node_operation_busy", "a node operation is already running"))
|
|
case errors.Is(err, nodecontrol.ErrNotFound):
|
|
writeError(w, r, newError(404, "not_found", "node task not found"))
|
|
default:
|
|
writeError(w, r, newError(503, "node_control_unavailable", "host node service is unavailable; inspect felis-node-control.service"))
|
|
}
|
|
}
|
|
|
|
// Fail closed for starts when the configured host service cannot report its maintenance state.
|
|
func NodeMaintenanceGuard(control NodeControl) func(context.Context) error {
|
|
return func(ctx context.Context) error {
|
|
tasks, err := control.List(ctx)
|
|
if err != nil {
|
|
return newError(503, "node_control_unavailable", "host node service is unavailable")
|
|
}
|
|
for _, task := range tasks {
|
|
if task.State == "running" {
|
|
return newError(409, "node_operation_busy", "node maintenance is in progress")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
}
|