Files
Felis/internal/api/handlers_node_control.go
T

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