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
@@ -14,11 +14,13 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"sultan/pkg/license"
|
||||
"sultan/pkg/policy"
|
||||
@@ -44,6 +46,28 @@ type Workload struct {
|
||||
Name string `json:"name"`
|
||||
Host string `json:"host"`
|
||||
Labels map[string]string `json:"labels"`
|
||||
// Meta: Zusatzdaten für Inventar und Störfall (Spec 5.3); nil bei
|
||||
// sultan.d-Diensten und älteren Agents. Ändert das Routing nie.
|
||||
Meta *WorkloadMeta `json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
// WorkloadMeta beschreibt, wo und wie ein Container läuft – automatisch
|
||||
// ermittelt, ohne zusätzliche Labels.
|
||||
type WorkloadMeta struct {
|
||||
ComposeProject string `json:"compose_project,omitempty"` // com.docker.compose.project
|
||||
ComposeService string `json:"compose_service,omitempty"` // com.docker.compose.service
|
||||
ComposeDir string `json:"compose_dir,omitempty"` // com.docker.compose.project.working_dir
|
||||
State string `json:"state,omitempty"` // running, exited, …
|
||||
StartedAt time.Time `json:"started_at,omitzero"` // letzter Start
|
||||
Health string `json:"health,omitempty"` // healthy, unhealthy, starting; leer = kein Healthcheck
|
||||
}
|
||||
|
||||
// HostInfo beschreibt den Knoten (Spec 5.3); nil bei älteren Agents.
|
||||
type HostInfo struct {
|
||||
FQDN string `json:"fqdn,omitempty"`
|
||||
OS string `json:"os,omitempty"`
|
||||
DockerVersion string `json:"docker_version,omitempty"`
|
||||
NetBirdIP string `json:"netbird_ip,omitempty"`
|
||||
}
|
||||
|
||||
// NodeState ist der vollständige Zustand eines Knotens (Payload von
|
||||
@@ -52,6 +76,29 @@ type NodeState struct {
|
||||
Workloads []Workload `json:"workloads"`
|
||||
// DownServices: Services (Originalnamen), deren Healthcheck fehlschlägt.
|
||||
DownServices []string `json:"down_services,omitempty"`
|
||||
// Host: Angaben zum Knoten; ändern das Routing nie.
|
||||
Host *HostInfo `json:"host_info,omitempty"`
|
||||
}
|
||||
|
||||
// clone liefert eine tiefe Kopie.
|
||||
func (s NodeState) clone() NodeState {
|
||||
out := NodeState{DownServices: slices.Clone(s.DownServices)}
|
||||
if s.Workloads != nil {
|
||||
out.Workloads = make([]Workload, len(s.Workloads))
|
||||
}
|
||||
for i, w := range s.Workloads {
|
||||
w.Labels = maps.Clone(w.Labels)
|
||||
if w.Meta != nil {
|
||||
m := *w.Meta
|
||||
w.Meta = &m
|
||||
}
|
||||
out.Workloads[i] = w
|
||||
}
|
||||
if s.Host != nil {
|
||||
h := *s.Host
|
||||
out.Host = &h
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// NodeContext ist, was die Policy über einen Knoten wissen muss.
|
||||
@@ -179,6 +226,17 @@ func (r *Registry) Nodes() []string {
|
||||
return r.nodeIDs()
|
||||
}
|
||||
|
||||
// State liefert eine Kopie des zuletzt gemeldeten Zustands eines Knotens.
|
||||
func (r *Registry) State(nodeID string) (NodeState, bool) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
entry, ok := r.nodes[nodeID]
|
||||
if !ok {
|
||||
return NodeState{}, false
|
||||
}
|
||||
return entry.state.clone(), true
|
||||
}
|
||||
|
||||
// Handler liefert die Konfiguration für Traefiks HTTP-Provider aus
|
||||
// (?format=json|yaml, Default JSON).
|
||||
func (r *Registry) Handler() http.Handler {
|
||||
|
||||
@@ -441,3 +441,65 @@ func TestRegistry_NodeMaintenanceFromDirectory(t *testing.T) {
|
||||
t.Error("Knotenwartung aus dem Directory nicht angewendet")
|
||||
}
|
||||
}
|
||||
|
||||
// Zusatzdaten (Compose, Startzeit, Health, Host-Infos) werden mit dem
|
||||
// Zustand gespeichert, ändern das Routing aber nicht.
|
||||
func TestRegistry_StateKeepsMetadata(t *testing.T) {
|
||||
r, _, _ := newRegistry()
|
||||
plain := wp("wordpress", "100.64.0.1", "kunde.de")
|
||||
if err := r.Update(ctx, "n1", NodeState{Workloads: []Workload{plain}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
before, _ := json.Marshal(r.Config())
|
||||
|
||||
started := time.Date(2026, 10, 16, 8, 15, 30, 0, time.UTC)
|
||||
withMeta := plain
|
||||
withMeta.Meta = &WorkloadMeta{
|
||||
ComposeProject: "kunde-a", ComposeService: "wordpress", ComposeDir: "/srv/kunde-a",
|
||||
State: "running", StartedAt: started, Health: "healthy",
|
||||
}
|
||||
state := NodeState{
|
||||
Workloads: []Workload{withMeta},
|
||||
Host: &HostInfo{
|
||||
FQDN: "webdocker11.lan.r25dc.de", OS: "Debian GNU/Linux 13 (trixie)",
|
||||
DockerVersion: "29.8.2", NetBirdIP: "100.64.0.42",
|
||||
},
|
||||
}
|
||||
if err := r.Update(ctx, "n1", state); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if after, _ := json.Marshal(r.Config()); string(after) != string(before) {
|
||||
t.Errorf("Zusatzdaten ändern das Routing:\nvorher %s\nnachher %s", before, after)
|
||||
}
|
||||
got, ok := r.State("n1")
|
||||
if !ok || !reflect.DeepEqual(got, state) {
|
||||
t.Errorf("State(n1) = %+v, %v; erwartet %+v", got, ok, state)
|
||||
}
|
||||
if _, ok := r.State("gibts-nicht"); ok {
|
||||
t.Error("State für unbekannten Knoten")
|
||||
}
|
||||
|
||||
// Kopie: Änderungen am Ergebnis wirken nicht auf die Registry zurück.
|
||||
got.Workloads[0].Meta.ComposeDir = "/manipuliert"
|
||||
got.Host.FQDN = "manipuliert"
|
||||
again, _ := r.State("n1")
|
||||
if again.Workloads[0].Meta.ComposeDir != "/srv/kunde-a" || again.Host.FQDN != "webdocker11.lan.r25dc.de" {
|
||||
t.Errorf("State liefert keine Kopie: %+v", again)
|
||||
}
|
||||
}
|
||||
|
||||
// Ältere Agents schicken keine Zusatzdaten; JSON ohne die Felder bleibt gültig
|
||||
// und erzeugt keine leeren Objekte.
|
||||
func TestNodeState_JSONWithoutMetadata(t *testing.T) {
|
||||
var st NodeState
|
||||
if err := json.Unmarshal([]byte(`{"workloads":[{"name":"wp","host":"10.0.0.1","labels":{}}]}`), &st); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if st.Host != nil || st.Workloads[0].Meta != nil {
|
||||
t.Errorf("Zusatzdaten erfunden: %+v", st)
|
||||
}
|
||||
out, _ := json.Marshal(st)
|
||||
if want := `{"workloads":[{"name":"wp","host":"10.0.0.1","labels":{}}]}`; string(out) != want {
|
||||
t.Errorf("JSON = %s, erwartet %s", out, want)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user