build - prozess optimiert mit push nach zot

nginx-kill - > maint. site...
This commit is contained in:
br committed 2026-10-03 12:44:45 +02:00
1 parent 665135f87e
commit 64417d8353
19 files changed
+447 -61

No files matched your search

+62 -4
View File
@@ -1,6 +1,7 @@
// Package docker ist ein minimaler Client für die Docker Engine API über
// den Unix-Socket – genau das, was der Agent braucht: laufende Container
// mit Labels und Ports auflisten und Container-Events verfolgen. Bewusst
// den Unix-Socket – genau das, was der Agent braucht: Container mit Labels
// und Ports auflisten, Portzuordnungen gestoppter Container lesen und
// Container-Events verfolgen. Bewusst
// ohne Docker-SDK (schwere Abhängigkeit für zwei Endpunkte).
package docker
@@ -13,6 +14,9 @@ import (
"net"
"net/http"
"net/url"
"regexp"
"slices"
"strconv"
"strings"
)
@@ -85,9 +89,10 @@ func (c *Client) get(ctx context.Context, path string) (*http.Response, error) {
return resp, nil
}
// List liefert die laufenden Container.
// List liefert alle Container, auch gestoppte (State != "running"); bei
// diesen ist Ports leer, siehe PortBindings.
func (c *Client) List(ctx context.Context) ([]Container, error) {
resp, err := c.get(ctx, "/containers/json")
resp, err := c.get(ctx, "/containers/json?all=1")
if err != nil {
return nil, err
}
@@ -99,6 +104,59 @@ func (c *Client) List(ctx context.Context) ([]Container, error) {
return out, nil
}
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) {
if !validID.MatchString(id) {
return nil, fmt.Errorf("docker: ungültige Container-ID %q", id)
}
resp, err := c.get(ctx, "/containers/"+id+"/json")
if err != nil {
return nil, err
}
defer resp.Body.Close()
var info struct {
HostConfig struct {
PortBindings map[string][]struct {
HostIP string `json:"HostIp"`
HostPort string `json:"HostPort"`
} `json:"PortBindings"`
} `json:"HostConfig"`
}
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
return nil, fmt.Errorf("docker: Container %s: %w", id, err)
}
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)
if err != nil {
continue
}
if proto == "" {
proto = "tcp"
}
bindings := info.HostConfig.PortBindings[k]
if len(bindings) == 0 {
out = append(out, 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})
}
}
return out, nil
}
// Events verfolgt Container-Events, bis ctx endet oder der Stream abbricht.
// Der Event-Kanal wird danach geschlossen; errc liefert genau einen Wert
// (ctx.Err() bei Abbruch, sonst den Grund).
+48 -1
View File
@@ -33,7 +33,11 @@ func newFakeEngine(t *testing.T, handler func(w http.ResponseWriter, r *http.Req
t.Fatal(err)
}
mux := http.NewServeMux()
mux.HandleFunc("/containers/json", handler)
mux.HandleFunc("/containers/json", func(w http.ResponseWriter, r *http.Request) {
f.lastQ <- r.URL.RawQuery
handler(w, r)
})
mux.HandleFunc("/containers/{id}/json", handler)
mux.HandleFunc("/events", func(w http.ResponseWriter, r *http.Request) {
f.lastQ <- r.URL.RawQuery
w.Header().Set("Content-Type", "application/json")
@@ -75,6 +79,10 @@ func TestList(t *testing.T) {
if err != nil {
t.Fatalf("List: %v", err)
}
// Auch gestoppte Container: Sie werden zur Wartungsseite statt zu 404.
if q := <-f.lastQ; q != "all=1" {
t.Errorf("Query = %q, erwartet all=1", q)
}
want := []Container{
{
ID: "abc", Names: []string{"/wordpress"}, State: "running",
@@ -91,6 +99,45 @@ func TestList(t *testing.T) {
}
}
// Gestoppte Container haben in /containers/json keine Ports; die
// Zuordnung steht in HostConfig.PortBindings.
func TestPortBindings(t *testing.T) {
f := newFakeEngine(t, func(w http.ResponseWriter, r *http.Request) {
if r.PathValue("id") != "abc" {
http.Error(w, `{"message":"No such container"}`, http.StatusNotFound)
return
}
w.Write([]byte(`{"Id":"abc","State":{"Status":"exited"},"HostConfig":{"PortBindings":{
"80/tcp":[{"HostIp":"","HostPort":"9080"},{"HostIp":"::","HostPort":"9080"}],
"443/tcp":[{"HostIp":"127.0.0.1","HostPort":"9443"}],
"53/udp":[{"HostIp":"","HostPort":"5353"}],
"8080/tcp":[{"HostIp":"","HostPort":""}],
"9000/tcp":null}}}`))
})
c := &Client{Socket: f.path}
got, err := c.PortBindings(context.Background(), "abc")
if err != nil {
t.Fatalf("PortBindings: %v", err)
}
want := []Port{
{IP: "127.0.0.1", PrivatePort: 443, PublicPort: 9443, Type: "tcp"},
{IP: "", PrivatePort: 53, PublicPort: 5353, Type: "udp"},
{IP: "", PrivatePort: 80, PublicPort: 9080, Type: "tcp"},
{IP: "::", PrivatePort: 80, PublicPort: 9080, Type: "tcp"},
{IP: "", PrivatePort: 8080, Type: "tcp"}, // Host-Port erst beim Start vergeben
{IP: "", PrivatePort: 9000, Type: "tcp"},
}
if !reflect.DeepEqual(got, want) {
t.Errorf("PortBindings =\n%+v\nerwartet\n%+v", got, want)
}
if _, err := c.PortBindings(context.Background(), "weg"); err == nil || !strings.Contains(err.Error(), "404") {
t.Errorf("unbekannter Container: %v", err)
}
if _, err := c.PortBindings(context.Background(), "../x"); err == nil {
t.Error("ungültige ID akzeptiert")
}
}
func TestList_Errors(t *testing.T) {
f := newFakeEngine(t, func(w http.ResponseWriter, r *http.Request) {
http.Error(w, `{"message":"kaputt"}`, http.StatusInternalServerError)