faf8027890
build / build (push) Successful in 1m14s
containerState 里把 Config 映射成 map[string]string,真实返回是对象, 于是每次拓扑校验都报 invalid docker response——所有窗口都被误拒。
141 lines
4.1 KiB
Go
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
|
|
}
|