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 }