Accountant: Inventar, where, Checkmk, Abrechnung
This commit is contained in:
1 parent
9f16148cde
commit
dd93277750
59 files changed
+7522
-113
No files matched your search
@@ -0,0 +1,137 @@
|
||||
package accountant
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"sultan/pkg/auth"
|
||||
"sultan/pkg/ledger"
|
||||
"sultan/pkg/store"
|
||||
)
|
||||
|
||||
// PathReport ist der Endpunkt für Berichte der Payees.
|
||||
const PathReport = "/api/v1/report"
|
||||
|
||||
// maxReportSize begrenzt einen Bericht (signierter Body). Ein Eintrag je
|
||||
// Route ist einige hundert Bytes groß; 8 MiB reichen für zehntausende Routen.
|
||||
const maxReportSize = 8 << 20
|
||||
|
||||
// Report ist der vollständige Schnappschuss eines Payees (Spec 5.3) – kein
|
||||
// Delta: Was fehlt, gibt es beim Payee nicht mehr.
|
||||
type Report struct {
|
||||
Version string `json:"version"` // sultan-Version des Payees
|
||||
GeneratedAt time.Time `json:"generated_at"`
|
||||
Tenants []ReportTenant `json:"tenants"`
|
||||
Nodes []ReportNode `json:"nodes"`
|
||||
Routes []ReportRoute `json:"routes"`
|
||||
}
|
||||
|
||||
// ReportTenant ist ein Mandant des Payees.
|
||||
type ReportTenant struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Tier string `json:"tier"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// ReportNode ist ein Knoten (Backend-Host) des Payees.
|
||||
type ReportNode struct {
|
||||
ID string `json:"id"`
|
||||
Tenant string `json:"tenant"`
|
||||
Hostname string `json:"hostname"`
|
||||
FQDN string `json:"fqdn,omitempty"`
|
||||
IP string `json:"ip"` // Absender-IP beim Server
|
||||
Status string `json:"status"`
|
||||
Connected bool `json:"connected"`
|
||||
Maintenance bool `json:"maintenance,omitempty"`
|
||||
LastSeen time.Time `json:"last_seen,omitzero"`
|
||||
OS string `json:"os,omitempty"`
|
||||
DockerVersion string `json:"docker_version,omitempty"`
|
||||
NetBirdIP string `json:"netbird_ip,omitempty"`
|
||||
}
|
||||
|
||||
// ReportRoute ist ein gemeldeter Router mit seinem Ziel.
|
||||
type ReportRoute struct {
|
||||
Tenant string `json:"tenant"`
|
||||
Node string `json:"node"`
|
||||
Workload string `json:"workload"` // Container bzw. sultan.d/<name>
|
||||
Router string `json:"router"` // Name aus den Labels
|
||||
Protocol string `json:"protocol"` // http oder tcp
|
||||
Hosts []string `json:"hosts"`
|
||||
// Backend wie Traefik es erreicht: http://<ip>:<port> bzw. <ip>:<port> (tcp).
|
||||
Backend string `json:"backend"`
|
||||
// BackendFQDN: <fqdn>:<port> – zum Springen auf den Host im Störfall.
|
||||
BackendFQDN string `json:"backend_fqdn,omitempty"`
|
||||
// Active: Router steht in der Traefik-Konfiguration (nicht von der Policy
|
||||
// verworfen, z. B. fremde Domain oder Routenlimit).
|
||||
Active bool `json:"active"`
|
||||
// Down: Service gestoppt bzw. Healthcheck fehlgeschlagen (Wartungsseite).
|
||||
Down bool `json:"down,omitempty"`
|
||||
Product string `json:"product,omitempty"` // Label sultan.accountant.billing
|
||||
|
||||
ComposeProject string `json:"compose_project,omitempty"`
|
||||
ComposeService string `json:"compose_service,omitempty"`
|
||||
ComposeDir string `json:"compose_dir,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
StartedAt time.Time `json:"started_at,omitzero"`
|
||||
Health string `json:"health,omitempty"`
|
||||
}
|
||||
|
||||
// LabelBilling ordnet einer Route ein Produkt zu (Spec 5.3).
|
||||
const LabelBilling = "sultan.accountant.billing"
|
||||
|
||||
func (a *Accountant) handleReport(w http.ResponseWriter, r *http.Request) {
|
||||
pub, err := a.reportVerifier.VerifyUnregistered(r)
|
||||
if err != nil {
|
||||
code := auth.StatusCode(err)
|
||||
http.Error(w, http.StatusText(code), code)
|
||||
return
|
||||
}
|
||||
p, err := a.ledger.GetPayeeByPublicKey(r.Context(), auth.EncodePublicKey(pub))
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
a.log.Error("Payee nachschlagen", "err", err)
|
||||
http.Error(w, "interner Fehler", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
var rep Report
|
||||
if err := json.NewDecoder(r.Body).Decode(&rep); err != nil {
|
||||
http.Error(w, "ungültiger Bericht", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if rep.GeneratedAt.IsZero() {
|
||||
http.Error(w, "generated_at fehlt", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := StoreReport(r.Context(), a.ledger, p.ID, rep); err != nil {
|
||||
a.log.Error("Bericht speichern", "payee", p.ID, "err", err)
|
||||
http.Error(w, "interner Fehler", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
a.log.Debug("Bericht empfangen", "payee", p.ID, "routes", len(rep.Routes), "nodes", len(rep.Nodes))
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// StoreReport legt einen Bericht ab: letzter Stand (normalisiert: nur
|
||||
// bekannte Felder), Historie und automatisch angelegte Kunden für neue
|
||||
// Mandanten.
|
||||
func StoreReport(ctx context.Context, l *ledger.Ledger, payeeID string, rep Report) error {
|
||||
body, err := json.Marshal(rep)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := l.SaveReport(ctx, payeeID, body, RouteRecords(rep)); err != nil {
|
||||
return err
|
||||
}
|
||||
tenants := make([]ledger.TenantRef, 0, len(rep.Tenants))
|
||||
for _, t := range rep.Tenants {
|
||||
tenants = append(tenants, ledger.TenantRef{ID: t.ID, Name: t.Name})
|
||||
}
|
||||
return l.EnsureCustomers(ctx, payeeID, tenants)
|
||||
}
|
||||
Reference in new issue
Block a user