257 lines
7.2 KiB
Go
257 lines
7.2 KiB
Go
// 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, 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
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"io"
|
||
"net"
|
||
"net/http"
|
||
"net/url"
|
||
"regexp"
|
||
"slices"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
// 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"`
|
||
|
||
// 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.
|
||
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_.-]*$`)
|
||
|
||
// 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 Details{}, fmt.Errorf("docker: ungültige Container-ID %q", id)
|
||
}
|
||
resp, err := c.get(ctx, "/containers/"+id+"/json")
|
||
if err != nil {
|
||
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"`
|
||
HostPort string `json:"HostPort"`
|
||
} `json:"PortBindings"`
|
||
} `json:"HostConfig"`
|
||
}
|
||
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
|
||
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)
|
||
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 {
|
||
d.Ports = append(d.Ports, Port{PrivatePort: uint16(private), Type: proto})
|
||
continue
|
||
}
|
||
for _, b := range bindings {
|
||
public, _ := strconv.ParseUint(b.HostPort, 10, 16) // "" → 0
|
||
d.Ports = append(d.Ports, Port{IP: b.HostIP, PrivatePort: uint16(private), PublicPort: uint16(public), Type: proto})
|
||
}
|
||
}
|
||
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.
|
||
// 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
|
||
}
|