Files
knowledge-governance/internal/librarymaintenance/dockerapi.go
T
姚章浩 faf8027890
build / build (push) Successful in 1m14s
fix(library): 修 Docker inspect 反序列化,拓扑校验才读得到端口
containerState 里把 Config 映射成 map[string]string,真实返回是对象,
于是每次拓扑校验都报 invalid docker response——所有窗口都被误拒。
2026-10-29 00:56:22 +08:00

141 lines
4.1 KiB
Go

package librarymaintenance
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
"time"
)
// dockerAPI 是目标维护用到的 Docker Engine 最小客户端(unix socket)。
// 入口闸门与拓扑校验共用同一份实现:两处各自拼一套请求只会长出两套毛病。
type dockerAPI struct {
client *http.Client
}
func newDockerAPI(socket string, timeout time.Duration) (*dockerAPI, error) {
if socket == "" || timeout <= 0 {
return nil, errors.New("invalid docker api configuration")
}
info, err := os.Stat(socket)
if err != nil || info.Mode()&os.ModeSocket == 0 {
return nil, errors.New("local Docker socket unavailable")
}
transport := &http.Transport{DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
return (&net.Dialer{}).DialContext(ctx, "unix", socket)
}}
return &dockerAPI{client: &http.Client{Transport: transport, Timeout: timeout,
CheckRedirect: func(*http.Request, []*http.Request) error { return errors.New("docker redirects prohibited") }}}, nil
}
func (d *dockerAPI) Close() { d.client.CloseIdleConnections() }
func (d *dockerAPI) request(ctx context.Context, method, path string, out any) error {
req, err := http.NewRequestWithContext(ctx, method, "http://docker"+path, nil)
if err != nil {
return errors.New("invalid docker request")
}
resp, err := d.client.Do(req)
if err != nil {
return errors.New("docker request failed")
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("docker request rejected: %d", resp.StatusCode)
}
if out != nil {
data, readErr := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if readErr != nil || json.Unmarshal(data, out) != nil {
return errors.New("invalid docker response")
}
}
return nil
}
type containerSummary struct {
ID string `json:"Id"`
Labels map[string]string `json:"Labels"`
Ports []struct {
PrivatePort int `json:"PrivatePort"`
PublicPort int `json:"PublicPort"`
Type string `json:"Type"`
} `json:"Ports"`
}
func (d *dockerAPI) listProject(ctx context.Context, project string) ([]containerSummary, error) {
filters, _ := json.Marshal(map[string][]string{"label": {"com.docker.compose.project=" + project}})
var items []containerSummary
if err := d.request(ctx, "GET", "/containers/json?all=1&filters="+url.QueryEscape(string(filters)), &items); err != nil {
return nil, err
}
return items, nil
}
type containerState struct {
ID string `json:"Id"`
Config struct {
Labels map[string]string `json:"Labels"`
} `json:"Config"`
State struct {
Running bool
Status string
ExitCode int
Health *struct{ Status string }
} `json:"State"`
HostConfig struct {
PortBindings map[string][]struct {
HostIP string `json:"HostIp"`
HostPort string `json:"HostPort"`
} `json:"PortBindings"`
} `json:"HostConfig"`
NetworkSettings struct {
Ports map[string][]struct {
HostIP string `json:"HostIp"`
HostPort string `json:"HostPort"`
} `json:"Ports"`
} `json:"NetworkSettings"`
}
func (d *dockerAPI) inspect(ctx context.Context, id string) (containerState, error) {
var data containerState
if err := d.request(ctx, "GET", "/containers/"+id+"/json", &data); err != nil {
return containerState{}, err
}
return data, nil
}
// publishedPorts 返回容器对外发布的端口(HostConfig + NetworkSettings 都算)。
// 写入者只要发布了一个端口,就存在"绕开入口闸门直连"的通道。
func (c containerState) publishedPorts() []string {
seen := map[string]bool{}
out := make([]string, 0, 2)
add := func(hostIP, hostPort, containerPort string) {
if hostPort == "" || hostPort == "0" {
return
}
key := hostIP + ":" + hostPort + "->" + containerPort
if seen[key] {
return
}
seen[key] = true
out = append(out, key)
}
for port, bindings := range c.HostConfig.PortBindings {
for _, binding := range bindings {
add(binding.HostIP, binding.HostPort, port)
}
}
for port, bindings := range c.NetworkSettings.Ports {
for _, binding := range bindings {
add(binding.HostIP, binding.HostPort, port)
}
}
return out
}