Files

237 lines
7.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}