build - prozess optimiert mit push nach zot
nginx-kill - > maint. site...
This commit is contained in:
1 parent
665135f87e
commit
64417d8353
19 files changed
+447
-61
No files matched your search
+62
-4
@@ -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).
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user