// Package dkr wraps the Docker Engine API with the operations the migration // tool needs: reading a full container inventory, streaming data out of a // container's mounts, and streaming image layers. package dkr import ( "context" "fmt" "net" "net/http" "time" "github.com/docker/docker/api/types/system" "github.com/docker/docker/client" ) // Client is a connection to one Docker daemon. type Client struct { api *client.Client // Endpoint is the daemon address, shown in the UI. Endpoint string } // New connects to the daemon described by the standard DOCKER_* environment // variables, or to host when it is non-empty (e.g. unix:///var/run/docker.sock // or tcp://10.0.0.5:2375). func New(host string) (*Client, error) { opts := []client.Opt{client.FromEnv, client.WithAPIVersionNegotiation()} if host != "" { opts = append(opts, client.WithHost(host)) } api, err := client.NewClientWithOpts(opts...) if err != nil { return nil, fmt.Errorf("create docker client: %w", err) } return &Client{api: api, Endpoint: api.DaemonHost()}, nil } // Dialer opens one connection to a daemon's API socket. type Dialer func(ctx context.Context, network, addr string) (net.Conn, error) // NewTunnel connects to a daemon that is only reachable through dial, such as a // remote daemon behind an SSH connection. The HTTP host is a placeholder: every // connection comes from dial, so the address is never resolved. // // endpoint is what the UI displays, e.g. ssh://root@10.0.0.5. func NewTunnel(endpoint string, dial Dialer) (*Client, error) { // The transport is ours so that WithHost cannot leave a TCP dialer or the // environment's HTTP proxy in place; either would send API calls somewhere // other than through the tunnel. tr := &http.Transport{ DisableCompression: true, // Every connection through the tunnel costs one SSH channel, and sshd // allows ten per connection by default (MaxSessions). Capping the pool // keeps a parallel migration from exhausting them; extra calls wait. MaxConnsPerHost: 8, MaxIdleConnsPerHost: 4, IdleConnTimeout: 5 * time.Minute, } api, err := client.NewClientWithOpts( client.WithHTTPClient(&http.Client{Transport: tr}), client.WithHost("http://docker.tunnel.invalid"), client.WithAPIVersionNegotiation(), ) if err != nil { return nil, fmt.Errorf("create docker client: %w", err) } tr.Proxy = nil tr.DialContext = dial if endpoint == "" { endpoint = "tunnel" } return &Client{api: api, Endpoint: endpoint}, nil } // API exposes the underlying SDK client for callers that need an operation // this package does not wrap. func (c *Client) API() *client.Client { return c.api } // Close releases the daemon connection. func (c *Client) Close() error { return c.api.Close() } // Info returns daemon information, and doubles as a connectivity check. func (c *Client) Info(ctx context.Context) (system.Info, error) { return c.api.Info(ctx) } // Ping verifies the daemon is reachable and returns its version string. func (c *Client) Ping(ctx context.Context) (string, error) { v, err := c.api.ServerVersion(ctx) if err != nil { return "", err } return v.Version, nil }