Initial commit: DSH Web Manager
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user