Files
sultan/pkg/docker/docker.go
T
br 64417d8353 build - prozess optimiert mit push nach zot
nginx-kill - > maint. site...
2026-10-03 12:44:45 +02:00

199 lines
5.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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
// ohne Docker-SDK (schwere Abhängigkeit für zwei Endpunkte).
package docker
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"regexp"
"slices"
"strconv"
"strings"
)
// Container ist ein Eintrag aus GET /containers/json.
type Container struct {
ID string `json:"Id"`
Names []string `json:"Names"`
State string `json:"State"`
Labels map[string]string `json:"Labels"`
Ports []Port `json:"Ports"`
}
// Name liefert den Containernamen ohne führenden Schrägstrich.
func (c Container) Name() string {
if len(c.Names) == 0 {
return c.ID
}
return strings.TrimPrefix(c.Names[0], "/")
}
// Port ist eine Portzuordnung; PublicPort 0 = nicht veröffentlicht.
type Port struct {
IP string `json:"IP,omitempty"`
PrivatePort uint16 `json:"PrivatePort"`
PublicPort uint16 `json:"PublicPort,omitempty"`
Type string `json:"Type"`
}
// Event ist ein Eintrag aus GET /events.
type Event struct {
Type string `json:"Type"`
Action string `json:"Action"`
Actor Actor `json:"Actor"`
}
// Actor beschreibt das betroffene Objekt eines Events.
type Actor struct {
ID string `json:"ID"`
Attributes map[string]string `json:"Attributes,omitempty"`
}
// Client spricht die Engine API über Socket (z. B. /var/run/docker.sock).
type Client struct {
Socket string
}
func (c *Client) http() *http.Client {
return &http.Client{Transport: &http.Transport{
DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
var d net.Dialer
return d.DialContext(ctx, "unix", c.Socket)
},
}}
}
func (c *Client) get(ctx context.Context, path string) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://docker"+path, nil)
if err != nil {
return nil, err
}
resp, err := c.http().Do(req)
if err != nil {
return nil, fmt.Errorf("docker: %s: %w", path, err)
}
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
resp.Body.Close()
return nil, fmt.Errorf("docker: %s: Status %d: %s", path, resp.StatusCode, strings.TrimSpace(string(body)))
}
return resp, nil
}
// 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?all=1")
if err != nil {
return nil, err
}
defer resp.Body.Close()
var out []Container
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
return nil, fmt.Errorf("docker: Containerliste: %w", err)
}
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).
func (c *Client) Events(ctx context.Context) (<-chan Event, <-chan error) {
events := make(chan Event)
errc := make(chan error, 1)
go func() {
defer close(events)
filters := url.QueryEscape(`{"type":["container"]}`)
resp, err := c.get(ctx, "/events?filters="+filters)
if err != nil {
errc <- err
return
}
defer resp.Body.Close()
dec := json.NewDecoder(resp.Body)
for {
var ev Event
if err := dec.Decode(&ev); err != nil {
switch {
case ctx.Err() != nil:
errc <- ctx.Err()
case errors.Is(err, io.EOF):
errc <- errors.New("docker: Event-Stream beendet")
default:
errc <- fmt.Errorf("docker: Event-Stream: %w", err)
}
return
}
select {
case events <- ev:
case <-ctx.Done():
errc <- ctx.Err()
return
}
}
}()
return events, errc
}