package adminapi import ( "context" "net/http" "github.com/dariuszjaniak/warp/server/internal/federation" "github.com/dariuszjaniak/warp/server/internal/httputil" "github.com/dariuszjaniak/warp/server/internal/storage" "github.com/dariuszjaniak/warp/server/internal/userapi" ) // FederationStatus is what the admin API can see of the running federation // module: whether it is up, and the delivery health of every server it // sends to. type FederationStatus interface { Running() bool Destinations(ctx context.Context) ([]storage.DestinationSummary, error) ResetBackoff(ctx context.Context, serverName string) error } // destinationResponse is one server in the destinations listing. type destinationResponse struct { ServerName string `json:"server_name"` // Failures is how many sends in a row have failed; zero is healthy. Failures int `json:"failures"` // RetryAtMS is when the next attempt is due, or 0 for now. RetryAtMS int64 `json:"retry_at_ms"` LastSuccessMS int64 `json:"last_success_ms"` LastFailureMS int64 `json:"last_failure_ms"` LastError string `json:"last_error,omitempty"` // QueuedPDUs and QueuedEDUs are the queue depth: what this server // still owes the destination. QueuedPDUs int `json:"queued_pdus"` QueuedEDUs int `json:"queued_edus"` } type destinationsResponse struct { Running bool `json:"running"` Destinations []destinationResponse `json:"destinations"` } // requireFederationStatus refuses on a server built without federation. func (a *AdminAPI) requireFederationStatus() error { if a.fedStatus == nil { return httputil.NotFound("This server was started without federation (it has no signing key)") } return nil } // handleGetDestinations implements GET /_warp/admin/v1/federation/destinations: // per-server queue depth and backoff, the panel's federation health view. func (a *AdminAPI) handleGetDestinations(w http.ResponseWriter, r *http.Request, _ userapi.Authenticated) error { if err := a.requireFederationStatus(); err != nil { return err } all, err := a.fedStatus.Destinations(r.Context()) if err != nil { return err } resp := destinationsResponse{ Running: a.fedStatus.Running(), Destinations: make([]destinationResponse, 0, len(all)), } for _, d := range all { resp.Destinations = append(resp.Destinations, destinationResponse{ ServerName: d.ServerName, Failures: d.Failures, RetryAtMS: d.RetryAtMS, LastSuccessMS: d.LastSuccessMS, LastFailureMS: d.LastFailureMS, LastError: d.LastError, QueuedPDUs: d.QueuedPDUs, QueuedEDUs: d.QueuedEDUs, }) } httputil.NoCache(w) httputil.WriteJSON(w, http.StatusOK, resp) return nil } // handleResetDestination implements // POST /_warp/admin/v1/federation/destinations/{serverName}/reset: forget a // server's backoff and try it now, for an operator who knows it is back. func (a *AdminAPI) handleResetDestination(w http.ResponseWriter, r *http.Request, auth userapi.Authenticated) error { if err := a.requireFederationStatus(); err != nil { return err } serverName := r.PathValue("serverName") if _, err := federation.ParseServerName(serverName); err != nil { return httputil.InvalidParam("%q is not a server name", serverName) } if err := a.fedStatus.ResetBackoff(r.Context(), serverName); err != nil { return err } a.audit(r.Context(), auth, auditDestinationReset, serverName, nil) httputil.WriteJSON(w, http.StatusOK, struct{}{}) return nil }