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
+71
-13
@@ -1,7 +1,8 @@
|
||||
// Package docker ist ein minimaler Client für die Docker Engine API über
|
||||
// den Unix-Socket – genau das, was der Agent braucht: Container mit Labels
|
||||
// und Ports auflisten, Portzuordnungen gestoppter Container lesen und
|
||||
// Container-Events verfolgen. Bewusst
|
||||
// und Ports auflisten, Details (Portzuordnungen auch gestoppter Container,
|
||||
// Startzeit, Health) lesen, Host-Infos abfragen und Container-Events
|
||||
// verfolgen. Bewusst
|
||||
// ohne Docker-SDK (schwere Abhängigkeit für zwei Endpunkte).
|
||||
package docker
|
||||
|
||||
@@ -18,6 +19,7 @@ import (
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Container ist ein Eintrag aus GET /containers/json.
|
||||
@@ -27,6 +29,10 @@ type Container struct {
|
||||
State string `json:"State"`
|
||||
Labels map[string]string `json:"Labels"`
|
||||
Ports []Port `json:"Ports"`
|
||||
|
||||
// Aus Inspect ergänzt (nicht Teil von /containers/json).
|
||||
StartedAt time.Time `json:"-"`
|
||||
Health string `json:"-"`
|
||||
}
|
||||
|
||||
// Name liefert den Containernamen ohne führenden Schrägstrich.
|
||||
@@ -106,19 +112,36 @@ func (c *Client) List(ctx context.Context) ([]Container, error) {
|
||||
|
||||
var validID = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_.-]*$`)
|
||||
|
||||
// PortBindings liefert die konfigurierten Portzuordnungen eines Containers
|
||||
// (HostConfig.PortBindings) – auch wenn er gestoppt ist. Ohne festen
|
||||
// Host-Port ist PublicPort 0. Sortiert nach "<port>/<proto>".
|
||||
func (c *Client) PortBindings(ctx context.Context, id string) ([]Port, error) {
|
||||
// Details sind die Angaben aus GET /containers/{id}/json, die der Agent
|
||||
// braucht.
|
||||
type Details struct {
|
||||
// Ports: konfigurierte Zuordnungen (HostConfig.PortBindings) – auch bei
|
||||
// gestoppten Containern. Ohne festen Host-Port ist PublicPort 0.
|
||||
// Sortiert nach "<port>/<proto>".
|
||||
Ports []Port
|
||||
// StartedAt: letzter Start; Nullzeit, wenn nie gestartet.
|
||||
StartedAt time.Time
|
||||
// Health: healthy, unhealthy, starting; leer ohne Healthcheck.
|
||||
Health string
|
||||
}
|
||||
|
||||
// Inspect liefert die Details eines Containers.
|
||||
func (c *Client) Inspect(ctx context.Context, id string) (Details, error) {
|
||||
if !validID.MatchString(id) {
|
||||
return nil, fmt.Errorf("docker: ungültige Container-ID %q", id)
|
||||
return Details{}, fmt.Errorf("docker: ungültige Container-ID %q", id)
|
||||
}
|
||||
resp, err := c.get(ctx, "/containers/"+id+"/json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return Details{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var info struct {
|
||||
State struct {
|
||||
StartedAt time.Time `json:"StartedAt"`
|
||||
Health *struct {
|
||||
Status string `json:"Status"`
|
||||
} `json:"Health"`
|
||||
} `json:"State"`
|
||||
HostConfig struct {
|
||||
PortBindings map[string][]struct {
|
||||
HostIP string `json:"HostIp"`
|
||||
@@ -127,14 +150,20 @@ func (c *Client) PortBindings(ctx context.Context, id string) ([]Port, error) {
|
||||
} `json:"HostConfig"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
|
||||
return nil, fmt.Errorf("docker: Container %s: %w", id, err)
|
||||
return Details{}, fmt.Errorf("docker: Container %s: %w", id, err)
|
||||
}
|
||||
var d Details
|
||||
if !info.State.StartedAt.IsZero() {
|
||||
d.StartedAt = info.State.StartedAt.UTC()
|
||||
}
|
||||
if info.State.Health != nil {
|
||||
d.Health = info.State.Health.Status
|
||||
}
|
||||
keys := make([]string, 0, len(info.HostConfig.PortBindings))
|
||||
for k := range info.HostConfig.PortBindings {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
slices.Sort(keys)
|
||||
var out []Port
|
||||
for _, k := range keys {
|
||||
port, proto, _ := strings.Cut(k, "/")
|
||||
private, err := strconv.ParseUint(port, 10, 16)
|
||||
@@ -146,15 +175,44 @@ func (c *Client) PortBindings(ctx context.Context, id string) ([]Port, error) {
|
||||
}
|
||||
bindings := info.HostConfig.PortBindings[k]
|
||||
if len(bindings) == 0 {
|
||||
out = append(out, Port{PrivatePort: uint16(private), Type: proto})
|
||||
d.Ports = append(d.Ports, Port{PrivatePort: uint16(private), Type: proto})
|
||||
continue
|
||||
}
|
||||
for _, b := range bindings {
|
||||
public, _ := strconv.ParseUint(b.HostPort, 10, 16) // "" → 0
|
||||
out = append(out, Port{IP: b.HostIP, PrivatePort: uint16(private), PublicPort: uint16(public), Type: proto})
|
||||
d.Ports = append(d.Ports, Port{IP: b.HostIP, PrivatePort: uint16(private), PublicPort: uint16(public), Type: proto})
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// PortBindings liefert die konfigurierten Portzuordnungen eines Containers
|
||||
// (siehe Details.Ports) – auch wenn er gestoppt ist.
|
||||
func (c *Client) PortBindings(ctx context.Context, id string) ([]Port, error) {
|
||||
d, err := c.Inspect(ctx, id)
|
||||
return d.Ports, err
|
||||
}
|
||||
|
||||
// Info sind die Host-Angaben aus GET /info. OperatingSystem ist das des
|
||||
// Hosts – auch wenn der Agent selbst in einem Container läuft.
|
||||
type Info struct {
|
||||
Name string `json:"Name"`
|
||||
OperatingSystem string `json:"OperatingSystem"`
|
||||
ServerVersion string `json:"ServerVersion"`
|
||||
}
|
||||
|
||||
// Info fragt die Host-Angaben ab.
|
||||
func (c *Client) Info(ctx context.Context) (Info, error) {
|
||||
resp, err := c.get(ctx, "/info")
|
||||
if err != nil {
|
||||
return Info{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var info Info
|
||||
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
|
||||
return Info{}, fmt.Errorf("docker: Info: %w", err)
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// Events verfolgt Container-Events, bis ctx endet oder der Stream abbricht.
|
||||
|
||||
@@ -38,6 +38,7 @@ func newFakeEngine(t *testing.T, handler func(w http.ResponseWriter, r *http.Req
|
||||
handler(w, r)
|
||||
})
|
||||
mux.HandleFunc("/containers/{id}/json", handler)
|
||||
mux.HandleFunc("/info", handler)
|
||||
mux.HandleFunc("/events", func(w http.ResponseWriter, r *http.Request) {
|
||||
f.lastQ <- r.URL.RawQuery
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -199,3 +200,63 @@ func TestEvents_StreamEnds(t *testing.T) {
|
||||
t.Error("erwartet Fehler bei beendetem Stream")
|
||||
}
|
||||
}
|
||||
|
||||
// Inspect liefert Ports (auch gestoppter Container), Startzeit und
|
||||
// Health-Status in einem Aufruf.
|
||||
func TestInspect(t *testing.T) {
|
||||
f := newFakeEngine(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.PathValue("id") {
|
||||
case "abc":
|
||||
w.Write([]byte(`{"Id":"abc","State":{"Status":"running","StartedAt":"2026-10-16T08:15:30.123456789Z",
|
||||
"Health":{"Status":"healthy"}},
|
||||
"HostConfig":{"PortBindings":{"80/tcp":[{"HostIp":"","HostPort":"9080"}]}}}`))
|
||||
case "nie":
|
||||
// Nie gestartet, kein Healthcheck: Docker liefert die Nullzeit.
|
||||
w.Write([]byte(`{"Id":"nie","State":{"Status":"created","StartedAt":"0001-01-01T00:00:00Z"},"HostConfig":{}}`))
|
||||
default:
|
||||
http.Error(w, `{"message":"No such container"}`, http.StatusNotFound)
|
||||
}
|
||||
})
|
||||
c := &Client{Socket: f.path}
|
||||
got, err := c.Inspect(context.Background(), "abc")
|
||||
if err != nil {
|
||||
t.Fatalf("Inspect: %v", err)
|
||||
}
|
||||
want := Details{
|
||||
Ports: []Port{{PrivatePort: 80, PublicPort: 9080, Type: "tcp"}},
|
||||
StartedAt: time.Date(2026, 10, 16, 8, 15, 30, 123456789, time.UTC),
|
||||
Health: "healthy",
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("Inspect =\n%+v\nerwartet\n%+v", got, want)
|
||||
}
|
||||
got, err = c.Inspect(context.Background(), "nie")
|
||||
if err != nil {
|
||||
t.Fatalf("Inspect: %v", err)
|
||||
}
|
||||
if !got.StartedAt.IsZero() || got.Health != "" || len(got.Ports) != 0 {
|
||||
t.Errorf("nie gestartet: %+v", got)
|
||||
}
|
||||
if _, err := c.Inspect(context.Background(), "weg"); err == nil || !strings.Contains(err.Error(), "404") {
|
||||
t.Errorf("unbekannter Container: %v", err)
|
||||
}
|
||||
if _, err := c.Inspect(context.Background(), "../x"); err == nil {
|
||||
t.Error("ungültige ID akzeptiert")
|
||||
}
|
||||
}
|
||||
|
||||
// Info liefert das Betriebssystem des Hosts (nicht des Agent-Containers) und
|
||||
// die Docker-Version.
|
||||
func TestInfo(t *testing.T) {
|
||||
f := newFakeEngine(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(`{"Name":"webdocker11","OperatingSystem":"Debian GNU/Linux 13 (trixie)","ServerVersion":"29.8.2","NCPU":4}`))
|
||||
})
|
||||
got, err := (&Client{Socket: f.path}).Info(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("Info: %v", err)
|
||||
}
|
||||
want := Info{Name: "webdocker11", OperatingSystem: "Debian GNU/Linux 13 (trixie)", ServerVersion: "29.8.2"}
|
||||
if got != want {
|
||||
t.Errorf("Info = %+v, erwartet %+v", got, want)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user