126 lines
3.6 KiB
Go
126 lines
3.6 KiB
Go
package adminapi
|
|
|
|
import (
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/iceBear67/simplepages/api"
|
|
"github.com/iceBear67/simplepages/internal/httpx"
|
|
"github.com/iceBear67/simplepages/internal/version"
|
|
)
|
|
|
|
// whoami handles GET /api/v1/whoami. Any valid key may call it; a CI job uses
|
|
// it to check that the token it was handed is the one it expected.
|
|
func (s *Server) whoami(w http.ResponseWriter, r *http.Request) error {
|
|
ident, err := s.identity(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
out := api.WhoAmI{
|
|
KeyID: ident.KeyID,
|
|
Scope: string(ident.Scope),
|
|
Name: ident.Name,
|
|
ExpiresAt: copyTime(ident.ExpiresAt),
|
|
}
|
|
if ident.ProjectID != nil {
|
|
// The identity carries the row id, not the name; the name is what a
|
|
// human wants to see. A missing row here would mean the project was
|
|
// deleted between authentication and now, in which case reporting an
|
|
// empty name is more honest than failing the request.
|
|
if p, err := s.DB.ProjectByID(r.Context(), *ident.ProjectID); err == nil {
|
|
out.Project = p.Name
|
|
}
|
|
}
|
|
noStore(w)
|
|
httpx.WriteJSON(w, http.StatusOK, out)
|
|
return nil
|
|
}
|
|
|
|
// systemInfo handles GET /api/v1/system/info, admin only.
|
|
func (s *Server) systemInfo(w http.ResponseWriter, r *http.Request) error {
|
|
counts, err := s.DB.Counts(r.Context())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
schema, err := s.DB.SchemaVersion(r.Context())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
linkMode := s.LinkMode
|
|
if linkMode == "" {
|
|
linkMode = "unknown"
|
|
}
|
|
uptime := int64(0)
|
|
if !s.Started.IsZero() {
|
|
uptime = int64(time.Since(s.Started).Seconds())
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, api.SystemInfo{
|
|
Version: version.Short(),
|
|
UptimeS: uptime,
|
|
Projects: counts.Projects,
|
|
Deployments: counts.Deployments,
|
|
Blobs: counts.Blobs,
|
|
CASBytes: counts.CASBytes,
|
|
LinkMode: linkMode,
|
|
SchemaVer: schema,
|
|
})
|
|
return nil
|
|
}
|
|
|
|
// gc handles POST /api/v1/gc, admin only.
|
|
//
|
|
// A collection pass also runs on a timer; this exists so an operator who needs
|
|
// the disk back does not have to wait for it, and so that a dry run can answer
|
|
// "what would you delete" before anything is deleted.
|
|
func (s *Server) gc(w http.ResponseWriter, r *http.Request) error {
|
|
var req api.GCRequest
|
|
if r.ContentLength != 0 {
|
|
if err := httpx.DecodeJSON(w, r, s.maxJSON(), &req); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
stats, err := s.Deploy.Collect(r.Context(), req.DryRun)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, stats)
|
|
return nil
|
|
}
|
|
|
|
// fsck handles POST /api/v1/fsck, admin only.
|
|
//
|
|
// Refcounts are maintained by triggers, so on a healthy server this always
|
|
// reports zero drift. It exists for the cases outside normal operation — a
|
|
// database restored from a backup, a schema touched by hand — because the
|
|
// collector trusts those counters, and a count that reads low is the one way
|
|
// this design can lose data.
|
|
func (s *Server) fsck(w http.ResponseWriter, r *http.Request) error {
|
|
var req api.FsckRequest
|
|
if r.ContentLength != 0 {
|
|
if err := httpx.DecodeJSON(w, r, s.maxJSON(), &req); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
rep, err := s.DB.Fsck(r.Context(), req.Repair)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
out := api.FsckReport{
|
|
Blobs: rep.Blobs,
|
|
DriftCount: rep.DriftCount,
|
|
Repaired: rep.Repaired,
|
|
Drift: make([]api.BlobDrift, 0, len(rep.Drift)),
|
|
}
|
|
for _, d := range rep.Drift {
|
|
out.Drift = append(out.Drift, api.BlobDrift{
|
|
Digest: d.Digest.String(), Stored: d.Stored, Actual: d.Actual,
|
|
})
|
|
}
|
|
if rep.DriftCount > 0 {
|
|
s.Log.WarnContext(r.Context(), "blob reference counts disagree with the manifests",
|
|
"blobs", rep.Blobs, "drift", rep.DriftCount, "repaired", rep.Repaired)
|
|
}
|
|
httpx.WriteJSON(w, http.StatusOK, out)
|
|
return nil
|
|
}
|