245 lines
6.3 KiB
Go
245 lines
6.3 KiB
Go
package docker
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
apiVersion = "/v1.47"
|
|
maxErrorBytes = 64 << 10
|
|
maxLogBytes = 1 << 20
|
|
)
|
|
|
|
type Client struct {
|
|
baseURL string
|
|
http *http.Client
|
|
}
|
|
|
|
type APIError struct {
|
|
StatusCode int
|
|
Message string
|
|
}
|
|
|
|
func (e *APIError) Error() string {
|
|
return fmt.Sprintf("docker API: HTTP %d: %s", e.StatusCode, e.Message)
|
|
}
|
|
|
|
func New(host string) *Client {
|
|
if host == "" {
|
|
host = "unix:///var/run/docker.sock"
|
|
}
|
|
transport := http.DefaultTransport.(*http.Transport).Clone()
|
|
if strings.HasPrefix(host, "unix://") {
|
|
socket := strings.TrimPrefix(host, "unix://")
|
|
transport.Proxy = nil
|
|
transport.DialContext = func(ctx context.Context, _, _ string) (net.Conn, error) {
|
|
return (&net.Dialer{}).DialContext(ctx, "unix", socket)
|
|
}
|
|
host = "http://docker"
|
|
} else if strings.HasPrefix(host, "tcp://") {
|
|
host = "http://" + strings.TrimPrefix(host, "tcp://")
|
|
}
|
|
return &Client{baseURL: strings.TrimRight(host, "/"), http: &http.Client{
|
|
Transport: transport,
|
|
CheckRedirect: func(_ *http.Request, _ []*http.Request) error { return http.ErrUseLastResponse },
|
|
}}
|
|
}
|
|
|
|
// Stream transfers ownership of a successful response body to the caller.
|
|
func (c *Client) Stream(ctx context.Context, method, path string, body io.Reader, contentType string) (*http.Response, error) {
|
|
if !strings.HasPrefix(path, "/") {
|
|
path = "/" + path
|
|
}
|
|
if path != apiVersion && !strings.HasPrefix(path, apiVersion+"/") && !strings.HasPrefix(path, apiVersion+"?") {
|
|
path = apiVersion + path
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if contentType != "" {
|
|
req.Header.Set("Content-Type", contentType)
|
|
}
|
|
resp, err := c.http.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
|
return resp, nil
|
|
}
|
|
defer resp.Body.Close()
|
|
data, _ := io.ReadAll(io.LimitReader(resp.Body, maxErrorBytes))
|
|
message := strings.TrimSpace(string(data))
|
|
var detail struct {
|
|
Message string `json:"message"`
|
|
}
|
|
if json.Unmarshal(data, &detail) == nil && detail.Message != "" {
|
|
message = detail.Message
|
|
}
|
|
if message == "" {
|
|
message = resp.Status
|
|
}
|
|
return nil, &APIError{StatusCode: resp.StatusCode, Message: message}
|
|
}
|
|
|
|
func (c *Client) Do(ctx context.Context, method, path string, body any, out any) error {
|
|
var input io.Reader
|
|
if body != nil {
|
|
data, err := json.Marshal(body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
input = bytes.NewReader(data)
|
|
}
|
|
resp, err := c.Stream(ctx, method, path, input, "application/json")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
if out == nil || resp.StatusCode == http.StatusNoContent {
|
|
return nil
|
|
}
|
|
return json.NewDecoder(resp.Body).Decode(out)
|
|
}
|
|
|
|
type Container struct {
|
|
ID string `json:"Id"`
|
|
Name string
|
|
Image string
|
|
Config struct {
|
|
Image string
|
|
Tty bool
|
|
Labels map[string]string
|
|
Env []string
|
|
}
|
|
State struct {
|
|
Status string
|
|
Running bool
|
|
Paused bool
|
|
Restarting bool
|
|
ExitCode int
|
|
Error string
|
|
Health *struct{ Status string }
|
|
}
|
|
NetworkSettings struct {
|
|
IPAddress string
|
|
Networks map[string]struct {
|
|
IPAddress string
|
|
Gateway string
|
|
NetworkID string
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Client) Inspect(ctx context.Context, id string) (*Container, error) {
|
|
var container Container
|
|
if err := c.Do(ctx, http.MethodGet, "/containers/"+url.PathEscape(id)+"/json", nil, &container); err != nil {
|
|
return nil, err
|
|
}
|
|
return &container, nil
|
|
}
|
|
|
|
func (c *Client) Logs(ctx context.Context, id string) (string, error) {
|
|
resp, err := c.Stream(ctx, http.MethodGet, "/containers/"+url.PathEscape(id)+"/logs?stdout=1&stderr=1&follow=0", nil, "")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
reader := bufio.NewReader(resp.Body)
|
|
header, err := reader.Peek(8)
|
|
if err != nil && err != io.EOF {
|
|
return "", err
|
|
}
|
|
if len(header) == 0 || header[0] > 2 {
|
|
data, err := io.ReadAll(io.LimitReader(reader, maxLogBytes))
|
|
return string(data), err
|
|
}
|
|
var output bytes.Buffer
|
|
for output.Len() < maxLogBytes {
|
|
var frame [8]byte
|
|
_, err := io.ReadFull(reader, frame[:])
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return output.String(), fmt.Errorf("docker logs header: %w", err)
|
|
}
|
|
if frame[0] > 2 || frame[1] != 0 || frame[2] != 0 || frame[3] != 0 {
|
|
return output.String(), fmt.Errorf("docker logs: invalid stream header")
|
|
}
|
|
size := int64(binary.BigEndian.Uint32(frame[4:]))
|
|
remaining := int64(maxLogBytes - output.Len())
|
|
if size > remaining {
|
|
size = remaining
|
|
}
|
|
if _, err := io.CopyN(&output, reader, size); err != nil {
|
|
return output.String(), fmt.Errorf("docker logs payload: %w", err)
|
|
}
|
|
}
|
|
return output.String(), nil
|
|
}
|
|
|
|
func (c *Client) Build(ctx context.Context, tar io.Reader, version, tag string, onLog func(string)) (string, error) {
|
|
args, err := json.Marshal(map[string]string{"DSH_VERSION": version})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
query := url.Values{"t": {tag}, "buildargs": {string(args)}, "rm": {"1"}}
|
|
resp, err := c.Stream(ctx, http.MethodPost, "/build?"+query.Encode(), tar, "application/x-tar")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
scanner := bufio.NewScanner(resp.Body)
|
|
scanner.Buffer(make([]byte, 4096), 1<<20)
|
|
var imageID string
|
|
for scanner.Scan() {
|
|
line := bytes.TrimSpace(scanner.Bytes())
|
|
if len(line) == 0 {
|
|
continue
|
|
}
|
|
var event struct {
|
|
Stream string `json:"stream"`
|
|
Error string `json:"error"`
|
|
ErrorDetail struct {
|
|
Message string `json:"message"`
|
|
} `json:"errorDetail"`
|
|
Aux struct {
|
|
ID string `json:"ID"`
|
|
} `json:"aux"`
|
|
}
|
|
if err := json.Unmarshal(line, &event); err != nil {
|
|
return "", fmt.Errorf("docker build event: %w", err)
|
|
}
|
|
if event.ErrorDetail.Message != "" {
|
|
return "", fmt.Errorf("docker build: %s", event.ErrorDetail.Message)
|
|
}
|
|
if event.Error != "" {
|
|
return "", fmt.Errorf("docker build: %s", event.Error)
|
|
}
|
|
if event.Stream != "" && onLog != nil {
|
|
onLog(event.Stream)
|
|
}
|
|
if event.Aux.ID != "" {
|
|
imageID = event.Aux.ID
|
|
}
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
return "", fmt.Errorf("docker build stream: %w", err)
|
|
}
|
|
if imageID == "" {
|
|
return "", fmt.Errorf("docker build: response ended without image ID")
|
|
}
|
|
return imageID, nil
|
|
}
|