237 lines
7.0 KiB
Go
237 lines
7.0 KiB
Go
package accountant
|
||
|
||
// Abfragen auf dem Accountant (Spec 5.3, Roadmap 6d Schritt 1): where – wo
|
||
// läuft eine URL, seit wann, wo vorher? – und routes – alle aktuellen
|
||
// Routen. Aktuell = letzter Bericht je Payee; seit wann/vorher = Historie.
|
||
|
||
import (
|
||
"cmp"
|
||
"context"
|
||
"encoding/json"
|
||
"slices"
|
||
"strings"
|
||
"time"
|
||
|
||
"sultan/pkg/ledger"
|
||
)
|
||
|
||
// RouteRecords übersetzt die Routen eines Berichts für die Historie.
|
||
func RouteRecords(rep Report) []ledger.RouteRecord {
|
||
out := make([]ledger.RouteRecord, 0, len(rep.Routes))
|
||
for _, r := range rep.Routes {
|
||
out = append(out, ledger.RouteRecord{
|
||
Tenant: r.Tenant, Node: r.Node, Protocol: r.Protocol, Router: r.Router, Hosts: r.Hosts,
|
||
Backend: r.Backend, BackendFQDN: r.BackendFQDN, Workload: r.Workload, ComposeDir: r.ComposeDir, Product: r.Product,
|
||
})
|
||
}
|
||
return out
|
||
}
|
||
|
||
// CurrentRoute ist eine Route aus dem letzten Bericht eines Payees.
|
||
type CurrentRoute struct {
|
||
Payee string
|
||
ReportAt time.Time // Eingang des Berichts
|
||
Route ReportRoute
|
||
Node ReportNode // Knoten der Route (leer, falls nicht im Bericht)
|
||
TenantName string
|
||
// Customer: zugeordneter Kunde (nil = keinem Kunden zugeordnet).
|
||
Customer *ledger.Customer
|
||
Since time.Time // Beginn des Zeitraums an diesem Ort (Historie)
|
||
// Previous: frühere, beendete Orte derselben Route (Mandant, Protokoll,
|
||
// Router), neueste zuerst.
|
||
Previous []ledger.RouteSpan
|
||
}
|
||
|
||
// WhereResult ist das Ergebnis von Where.
|
||
type WhereResult struct {
|
||
Current []CurrentRoute
|
||
// Gone: passende Routen, die es nirgends mehr gibt (neueste zuerst).
|
||
Gone []ledger.RouteSpan
|
||
}
|
||
|
||
// RoutesFilter schränkt Routes ein; leere Felder filtern nicht.
|
||
type RoutesFilter struct {
|
||
Tenant string
|
||
Payee string
|
||
}
|
||
|
||
func spanKey(tenant, node, protocol, router string) string {
|
||
return tenant + "\x00" + node + "\x00" + protocol + "\x00" + router
|
||
}
|
||
|
||
func routeKey(tenant, protocol, router string) string {
|
||
return tenant + "\x00" + protocol + "\x00" + router
|
||
}
|
||
|
||
// currentRoutes liest die letzten Berichte aller Payees und ergänzt den
|
||
// Beginn je Ort aus der Historie.
|
||
func currentRoutes(ctx context.Context, l *ledger.Ledger, keep func(ReportRoute) bool) ([]CurrentRoute, error) {
|
||
payees, err := l.ListPayees(ctx)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
open, err := l.RouteHistory(ctx, ledger.HistoryFilter{OpenOnly: true})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
resolver, err := l.CustomerResolver(ctx)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
customers := map[string]*ledger.Customer{}
|
||
cs, err := l.ListCustomers(ctx)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
for i := range cs {
|
||
customers[cs[i].ID] = &cs[i]
|
||
}
|
||
since := map[string]time.Time{}
|
||
for _, s := range open {
|
||
since[s.Payee+"\x00"+spanKey(s.Tenant, s.Node, s.Protocol, s.Router)] = s.FirstSeen
|
||
}
|
||
var out []CurrentRoute
|
||
for _, p := range payees {
|
||
body, at, err := l.LatestReport(ctx, p.ID)
|
||
if err != nil {
|
||
continue // noch kein Bericht
|
||
}
|
||
var rep Report
|
||
if err := json.Unmarshal(body, &rep); err != nil {
|
||
return nil, err
|
||
}
|
||
nodes := map[string]ReportNode{}
|
||
for _, n := range rep.Nodes {
|
||
nodes[n.ID] = n
|
||
}
|
||
names := map[string]string{}
|
||
for _, t := range rep.Tenants {
|
||
names[t.ID] = t.Name
|
||
}
|
||
for _, r := range rep.Routes {
|
||
if !keep(r) {
|
||
continue
|
||
}
|
||
out = append(out, CurrentRoute{
|
||
Payee: p.ID, ReportAt: at, Route: r, Node: nodes[r.Node], TenantName: names[r.Tenant],
|
||
Customer: customers[resolver.Resolve(p.ID, r.Tenant)],
|
||
Since: since[p.ID+"\x00"+spanKey(r.Tenant, r.Node, r.Protocol, r.Router)],
|
||
})
|
||
}
|
||
}
|
||
slices.SortFunc(out, func(a, b CurrentRoute) int {
|
||
return cmp.Or(strings.Compare(firstHost(a.Route.Hosts), firstHost(b.Route.Hosts)),
|
||
strings.Compare(a.Payee, b.Payee), strings.Compare(a.Route.Node, b.Route.Node), strings.Compare(a.Route.Router, b.Route.Router))
|
||
})
|
||
return out, nil
|
||
}
|
||
|
||
func firstHost(hosts []string) string {
|
||
if len(hosts) == 0 {
|
||
return ""
|
||
}
|
||
return hosts[0]
|
||
}
|
||
|
||
// matches: ein Host enthält query, oder query ist die Mandanten-ID.
|
||
func matches(query, tenant string, hosts []string) bool {
|
||
if strings.EqualFold(tenant, query) {
|
||
return true
|
||
}
|
||
for _, h := range hosts {
|
||
if strings.Contains(strings.ToLower(h), query) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// Where sucht Routen, deren Host query enthält (ohne Groß-/Kleinschreibung)
|
||
// oder deren Mandant query heißt – aktuell mit früheren Orten, dazu
|
||
// passende Routen, die es nicht mehr gibt.
|
||
func Where(ctx context.Context, l *ledger.Ledger, query string) (WhereResult, error) {
|
||
query = strings.ToLower(strings.TrimSpace(query))
|
||
var res WhereResult
|
||
if query == "" {
|
||
return res, nil
|
||
}
|
||
cur, err := currentRoutes(ctx, l, func(r ReportRoute) bool { return matches(query, r.Tenant, r.Hosts) })
|
||
if err != nil {
|
||
return res, err
|
||
}
|
||
all, err := l.RouteHistory(ctx, ledger.HistoryFilter{})
|
||
if err != nil {
|
||
return res, err
|
||
}
|
||
ended := map[string][]ledger.RouteSpan{}
|
||
for _, s := range all {
|
||
if !s.Ended.IsZero() {
|
||
k := routeKey(s.Tenant, s.Protocol, s.Router)
|
||
ended[k] = append(ended[k], s)
|
||
}
|
||
}
|
||
newestFirst := func(a, b ledger.RouteSpan) int { return b.Ended.Compare(a.Ended) }
|
||
alive := map[string]bool{}
|
||
for i := range cur {
|
||
r := cur[i].Route
|
||
k := routeKey(r.Tenant, r.Protocol, r.Router)
|
||
alive[k] = true
|
||
for _, s := range ended[k] {
|
||
if s.Payee == cur[i].Payee && s.Node == r.Node {
|
||
continue // derselbe Ort früher einmal – kein Umzug
|
||
}
|
||
cur[i].Previous = append(cur[i].Previous, s)
|
||
}
|
||
slices.SortFunc(cur[i].Previous, newestFirst)
|
||
}
|
||
res.Current = cur
|
||
for k, spans := range ended {
|
||
if alive[k] {
|
||
continue
|
||
}
|
||
for _, s := range spans {
|
||
if matches(query, s.Tenant, s.Hosts) {
|
||
res.Gone = append(res.Gone, s)
|
||
}
|
||
}
|
||
}
|
||
slices.SortFunc(res.Gone, newestFirst)
|
||
return res, nil
|
||
}
|
||
|
||
// Routes liefert alle aktuellen Routen, sortiert nach Host.
|
||
func Routes(ctx context.Context, l *ledger.Ledger, f RoutesFilter) ([]CurrentRoute, error) {
|
||
cur, err := currentRoutes(ctx, l, func(r ReportRoute) bool { return f.Tenant == "" || r.Tenant == f.Tenant })
|
||
if err != nil || f.Payee == "" {
|
||
return cur, err
|
||
}
|
||
return slices.DeleteFunc(cur, func(c CurrentRoute) bool { return c.Payee != f.Payee }), nil
|
||
}
|
||
|
||
// ItemKey bezeichnet einen Posten: Mandant, Protokoll, Router (über alle
|
||
// Payees und Knoten – ein Umzug bleibt derselbe Posten).
|
||
type ItemKey struct {
|
||
Tenant, Protocol, Router string
|
||
}
|
||
|
||
// ItemsForHost liefert die Posten, zu denen host gehört (exakter Host, aus
|
||
// der Historie; ohne Groß-/Kleinschreibung), sortiert.
|
||
func ItemsForHost(ctx context.Context, l *ledger.Ledger, host string) ([]ItemKey, error) {
|
||
host = strings.ToLower(strings.TrimSpace(host))
|
||
spans, err := l.RouteHistory(ctx, ledger.HistoryFilter{Host: host})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var out []ItemKey
|
||
for _, s := range spans {
|
||
k := ItemKey{s.Tenant, s.Protocol, s.Router}
|
||
if slices.Contains(s.Hosts, host) && !slices.Contains(out, k) {
|
||
out = append(out, k)
|
||
}
|
||
}
|
||
slices.SortFunc(out, func(a, b ItemKey) int {
|
||
return cmp.Or(strings.Compare(a.Tenant, b.Tenant), strings.Compare(a.Router, b.Router), strings.Compare(a.Protocol, b.Protocol))
|
||
})
|
||
return out, nil
|
||
}
|