mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
Extract API client struct as standalone client.
Signed-off-by: David Calavera <david.calavera@gmail.com>
This commit is contained in:
parent
de7b80db03
commit
589df17a1a
4 changed files with 320 additions and 64 deletions
|
@ -6,14 +6,14 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"runtime"
|
||||
|
||||
"github.com/docker/docker/api/client/lib"
|
||||
"github.com/docker/docker/cli"
|
||||
"github.com/docker/docker/cliconfig"
|
||||
"github.com/docker/docker/dockerversion"
|
||||
"github.com/docker/docker/opts"
|
||||
"github.com/docker/docker/pkg/sockets"
|
||||
"github.com/docker/docker/pkg/term"
|
||||
"github.com/docker/docker/pkg/tlsconfig"
|
||||
)
|
||||
|
@ -24,13 +24,6 @@ type DockerCli struct {
|
|||
// initializing closure
|
||||
init func() error
|
||||
|
||||
// proto holds the client protocol i.e. unix.
|
||||
proto string
|
||||
// addr holds the client address.
|
||||
addr string
|
||||
// basePath holds the path to prepend to the requests
|
||||
basePath string
|
||||
|
||||
// configFile has the client configuration file
|
||||
configFile *cliconfig.ConfigFile
|
||||
// in holds the input stream and closer (io.ReadCloser) for the client.
|
||||
|
@ -41,11 +34,6 @@ type DockerCli struct {
|
|||
err io.Writer
|
||||
// keyFile holds the key file as a string.
|
||||
keyFile string
|
||||
// tlsConfig holds the TLS configuration for the client, and will
|
||||
// set the scheme to https in NewDockerCli if present.
|
||||
tlsConfig *tls.Config
|
||||
// scheme holds the scheme of the client i.e. https.
|
||||
scheme string
|
||||
// inFd holds the file descriptor of the client's STDIN (if valid).
|
||||
inFd uintptr
|
||||
// outFd holds file descriptor of the client's STDOUT (if valid).
|
||||
|
@ -54,6 +42,22 @@ type DockerCli struct {
|
|||
isTerminalIn bool
|
||||
// isTerminalOut indicates whether the client's STDOUT is a TTY
|
||||
isTerminalOut bool
|
||||
// client is the http client that performs all API operations
|
||||
client *lib.Client
|
||||
|
||||
// DEPRECATED OPTIONS TO MAKE THE CLIENT COMPILE
|
||||
// TODO: Remove
|
||||
// proto holds the client protocol i.e. unix.
|
||||
proto string
|
||||
// addr holds the client address.
|
||||
addr string
|
||||
// basePath holds the path to prepend to the requests
|
||||
basePath string
|
||||
// tlsConfig holds the TLS configuration for the client, and will
|
||||
// set the scheme to https in NewDockerCli if present.
|
||||
tlsConfig *tls.Config
|
||||
// scheme holds the scheme of the client i.e. https.
|
||||
scheme string
|
||||
// transport holds the client transport instance.
|
||||
transport *http.Transport
|
||||
}
|
||||
|
@ -98,50 +102,35 @@ func NewDockerCli(in io.ReadCloser, out, err io.Writer, clientFlags *cli.ClientF
|
|||
}
|
||||
|
||||
cli.init = func() error {
|
||||
|
||||
clientFlags.PostParse()
|
||||
configFile, e := cliconfig.Load(cliconfig.ConfigDir())
|
||||
if e != nil {
|
||||
fmt.Fprintf(cli.err, "WARNING: Error loading config file:%v\n", e)
|
||||
}
|
||||
cli.configFile = configFile
|
||||
|
||||
hosts := clientFlags.Common.Hosts
|
||||
|
||||
switch len(hosts) {
|
||||
case 0:
|
||||
hosts = []string{os.Getenv("DOCKER_HOST")}
|
||||
case 1:
|
||||
// only accept one host to talk to
|
||||
default:
|
||||
return errors.New("Please specify only one -H")
|
||||
host, err := getServerHost(clientFlags.Common.Hosts, clientFlags.Common.TLSOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defaultHost := opts.DefaultTCPHost
|
||||
if clientFlags.Common.TLSOptions != nil {
|
||||
defaultHost = opts.DefaultTLSHost
|
||||
customHeaders := cli.configFile.HTTPHeaders
|
||||
if customHeaders == nil {
|
||||
customHeaders = map[string]string{}
|
||||
}
|
||||
customHeaders["User-Agent"] = "Docker-Client/" + dockerversion.Version + " (" + runtime.GOOS + ")"
|
||||
|
||||
var e error
|
||||
if hosts[0], e = opts.ParseHost(defaultHost, hosts[0]); e != nil {
|
||||
return e
|
||||
client, err := lib.NewClient(host, clientFlags.Common.TLSOptions, customHeaders)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cli.client = client
|
||||
|
||||
protoAddrParts := strings.SplitN(hosts[0], "://", 2)
|
||||
cli.proto, cli.addr = protoAddrParts[0], protoAddrParts[1]
|
||||
|
||||
if cli.proto == "tcp" {
|
||||
// error is checked in pkg/parsers already
|
||||
parsed, _ := url.Parse("tcp://" + cli.addr)
|
||||
cli.addr = parsed.Host
|
||||
cli.basePath = parsed.Path
|
||||
}
|
||||
|
||||
if clientFlags.Common.TLSOptions != nil {
|
||||
cli.scheme = "https"
|
||||
var e error
|
||||
cli.tlsConfig, e = tlsconfig.Client(*clientFlags.Common.TLSOptions)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
} else {
|
||||
cli.scheme = "http"
|
||||
}
|
||||
// FIXME: Deprecated, only to keep the old code running.
|
||||
cli.transport = client.HTTPClient.Transport.(*http.Transport)
|
||||
cli.basePath = client.BasePath
|
||||
cli.addr = client.Addr
|
||||
cli.scheme = client.Scheme
|
||||
|
||||
if cli.in != nil {
|
||||
cli.inFd, cli.isTerminalIn = term.GetFdInfo(cli.in)
|
||||
|
@ -150,20 +139,27 @@ func NewDockerCli(in io.ReadCloser, out, err io.Writer, clientFlags *cli.ClientF
|
|||
cli.outFd, cli.isTerminalOut = term.GetFdInfo(cli.out)
|
||||
}
|
||||
|
||||
// The transport is created here for reuse during the client session.
|
||||
cli.transport = &http.Transport{
|
||||
TLSClientConfig: cli.tlsConfig,
|
||||
}
|
||||
sockets.ConfigureTCPTransport(cli.transport, cli.proto, cli.addr)
|
||||
|
||||
configFile, e := cliconfig.Load(cliconfig.ConfigDir())
|
||||
if e != nil {
|
||||
fmt.Fprintf(cli.err, "WARNING: Error loading config file:%v\n", e)
|
||||
}
|
||||
cli.configFile = configFile
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return cli
|
||||
}
|
||||
|
||||
func getServerHost(hosts []string, tlsOptions *tlsconfig.Options) (host string, err error) {
|
||||
switch len(hosts) {
|
||||
case 0:
|
||||
host = os.Getenv("DOCKER_HOST")
|
||||
case 1:
|
||||
host = hosts[0]
|
||||
default:
|
||||
return "", errors.New("Please specify only one -H")
|
||||
}
|
||||
|
||||
defaultHost := opts.DefaultTCPHost
|
||||
if tlsOptions != nil {
|
||||
defaultHost = opts.DefaultTLSHost
|
||||
}
|
||||
|
||||
host, err = opts.ParseHost(defaultHost, host)
|
||||
return
|
||||
}
|
||||
|
|
98
api/client/lib/client.go
Normal file
98
api/client/lib/client.go
Normal file
|
@ -0,0 +1,98 @@
|
|||
package lib
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api"
|
||||
"github.com/docker/docker/pkg/sockets"
|
||||
"github.com/docker/docker/pkg/tlsconfig"
|
||||
"github.com/docker/docker/pkg/version"
|
||||
)
|
||||
|
||||
// Client is the API client that performs all operations
|
||||
// against a docker server.
|
||||
type Client struct {
|
||||
// proto holds the client protocol i.e. unix.
|
||||
Proto string
|
||||
// addr holds the client address.
|
||||
Addr string
|
||||
// basePath holds the path to prepend to the requests
|
||||
BasePath string
|
||||
// scheme holds the scheme of the client i.e. https.
|
||||
Scheme string
|
||||
// httpClient holds the client transport instance. Exported to keep the old code running.
|
||||
HTTPClient *http.Client
|
||||
// version of the server to talk to.
|
||||
version version.Version
|
||||
// custom http headers configured by users
|
||||
customHTTPHeaders map[string]string
|
||||
}
|
||||
|
||||
// NewClient initializes a new API client
|
||||
// for the given host. It uses the tlsOptions
|
||||
// to decide whether to use a secure connection or not.
|
||||
// It also initializes the custom http headers to add to each request.
|
||||
func NewClient(host string, tlsOptions *tlsconfig.Options, httpHeaders map[string]string) (*Client, error) {
|
||||
return NewClientWithVersion(host, api.Version, tlsOptions, httpHeaders)
|
||||
}
|
||||
|
||||
// NewClientWithVersion initializes a new API client
|
||||
// for the given host and API version. It uses the tlsOptions
|
||||
// to decide whether to use a secure connection or not.
|
||||
// It also initializes the custom http headers to add to each request.
|
||||
func NewClientWithVersion(host string, version version.Version, tlsOptions *tlsconfig.Options, httpHeaders map[string]string) (*Client, error) {
|
||||
var (
|
||||
basePath string
|
||||
tlsConfig *tls.Config
|
||||
scheme = "http"
|
||||
protoAddrParts = strings.SplitN(host, "://", 2)
|
||||
proto, addr = protoAddrParts[0], protoAddrParts[1]
|
||||
)
|
||||
|
||||
if proto == "tcp" {
|
||||
parsed, err := url.Parse("tcp://" + addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addr = parsed.Host
|
||||
basePath = parsed.Path
|
||||
}
|
||||
|
||||
if tlsOptions != nil {
|
||||
scheme = "https"
|
||||
var err error
|
||||
tlsConfig, err = tlsconfig.Client(*tlsOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// The transport is created here for reuse during the client session.
|
||||
transport := &http.Transport{
|
||||
TLSClientConfig: tlsConfig,
|
||||
}
|
||||
sockets.ConfigureTCPTransport(transport, proto, addr)
|
||||
|
||||
return &Client{
|
||||
Addr: addr,
|
||||
BasePath: basePath,
|
||||
Scheme: scheme,
|
||||
HTTPClient: &http.Client{Transport: transport},
|
||||
version: version,
|
||||
customHTTPHeaders: httpHeaders,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getAPIPath returns the versioned request path to call the api.
|
||||
// It appends the query parameters to the path if they are not empty.
|
||||
func (cli *Client) getAPIPath(p string, query url.Values) string {
|
||||
apiPath := fmt.Sprintf("%s/v%s%s", cli.BasePath, cli.version, p)
|
||||
if len(query) > 0 {
|
||||
apiPath += "?" + query.Encode()
|
||||
}
|
||||
return apiPath
|
||||
}
|
155
api/client/lib/request.go
Normal file
155
api/client/lib/request.go
Normal file
|
@ -0,0 +1,155 @@
|
|||
package lib
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/utils"
|
||||
)
|
||||
|
||||
// ServerResponse is a wrapper for http API responses.
|
||||
type ServerResponse struct {
|
||||
body io.ReadCloser
|
||||
header http.Header
|
||||
statusCode int
|
||||
}
|
||||
|
||||
// HEAD sends an http request to the docker API using the method HEAD.
|
||||
func (cli *Client) HEAD(path string, query url.Values, headers map[string][]string) (*ServerResponse, error) {
|
||||
return cli.sendRequest("HEAD", path, query, nil, headers)
|
||||
}
|
||||
|
||||
// GET sends an http request to the docker API using the method GET.
|
||||
func (cli *Client) GET(path string, query url.Values, headers map[string][]string) (*ServerResponse, error) {
|
||||
return cli.sendRequest("GET", path, query, nil, headers)
|
||||
}
|
||||
|
||||
// POST sends an http request to the docker API using the method POST.
|
||||
func (cli *Client) POST(path string, query url.Values, body interface{}, headers map[string][]string) (*ServerResponse, error) {
|
||||
return cli.sendRequest("POST", path, query, body, headers)
|
||||
}
|
||||
|
||||
// POSTRaw sends the raw input to the docker API using the method POST.
|
||||
func (cli *Client) POSTRaw(path string, query url.Values, body io.Reader, headers map[string][]string) (*ServerResponse, error) {
|
||||
return cli.sendClientRequest("POST", path, query, body, headers)
|
||||
}
|
||||
|
||||
// PUT sends an http request to the docker API using the method PUT.
|
||||
func (cli *Client) PUT(path string, query url.Values, body interface{}, headers map[string][]string) (*ServerResponse, error) {
|
||||
return cli.sendRequest("PUT", path, query, body, headers)
|
||||
}
|
||||
|
||||
// DELETE sends an http request to the docker API using the method DELETE.
|
||||
func (cli *Client) DELETE(path string, query url.Values, headers map[string][]string) (*ServerResponse, error) {
|
||||
return cli.sendRequest("DELETE", path, query, nil, headers)
|
||||
}
|
||||
|
||||
func (cli *Client) sendRequest(method, path string, query url.Values, body interface{}, headers map[string][]string) (*ServerResponse, error) {
|
||||
params, err := encodeData(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if body != nil {
|
||||
if headers == nil {
|
||||
headers = make(map[string][]string)
|
||||
}
|
||||
headers["Content-Type"] = []string{"application/json"}
|
||||
}
|
||||
|
||||
return cli.sendClientRequest(method, path, query, params, headers)
|
||||
}
|
||||
|
||||
func (cli *Client) sendClientRequest(method, path string, query url.Values, in io.Reader, headers map[string][]string) (*ServerResponse, error) {
|
||||
serverResp := &ServerResponse{
|
||||
body: nil,
|
||||
statusCode: -1,
|
||||
}
|
||||
|
||||
expectedPayload := (method == "POST" || method == "PUT")
|
||||
if expectedPayload && in == nil {
|
||||
in = bytes.NewReader([]byte{})
|
||||
}
|
||||
|
||||
apiPath := cli.getAPIPath(path, query)
|
||||
req, err := http.NewRequest(method, apiPath, in)
|
||||
if err != nil {
|
||||
return serverResp, err
|
||||
}
|
||||
|
||||
// Add CLI Config's HTTP Headers BEFORE we set the Docker headers
|
||||
// then the user can't change OUR headers
|
||||
for k, v := range cli.customHTTPHeaders {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
||||
req.URL.Host = cli.Addr
|
||||
req.URL.Scheme = cli.Scheme
|
||||
|
||||
if headers != nil {
|
||||
for k, v := range headers {
|
||||
req.Header[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
if expectedPayload && req.Header.Get("Content-Type") == "" {
|
||||
req.Header.Set("Content-Type", "text/plain")
|
||||
}
|
||||
|
||||
resp, err := cli.HTTPClient.Do(req)
|
||||
if resp != nil {
|
||||
serverResp.statusCode = resp.StatusCode
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if utils.IsTimeout(err) || strings.Contains(err.Error(), "connection refused") || strings.Contains(err.Error(), "dial unix") {
|
||||
return serverResp, errConnectionFailed
|
||||
}
|
||||
|
||||
if cli.Scheme == "http" && strings.Contains(err.Error(), "malformed HTTP response") {
|
||||
return serverResp, fmt.Errorf("%v.\n* Are you trying to connect to a TLS-enabled daemon without TLS?", err)
|
||||
}
|
||||
if cli.Scheme == "https" && strings.Contains(err.Error(), "remote error: bad certificate") {
|
||||
return serverResp, fmt.Errorf("The server probably has client authentication (--tlsverify) enabled. Please check your TLS client certification settings: %v", err)
|
||||
}
|
||||
|
||||
return serverResp, fmt.Errorf("An error occurred trying to connect: %v", err)
|
||||
}
|
||||
|
||||
if serverResp.statusCode < 200 || serverResp.statusCode >= 400 {
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return serverResp, err
|
||||
}
|
||||
if len(body) == 0 {
|
||||
return serverResp, fmt.Errorf("Error: request returned %s for API route and version %s, check if the server supports the requested API version", http.StatusText(serverResp.statusCode), req.URL)
|
||||
}
|
||||
return serverResp, fmt.Errorf("Error response from daemon: %s", bytes.TrimSpace(body))
|
||||
}
|
||||
|
||||
serverResp.body = resp.Body
|
||||
serverResp.header = resp.Header
|
||||
return serverResp, nil
|
||||
}
|
||||
|
||||
func encodeData(data interface{}) (*bytes.Buffer, error) {
|
||||
params := bytes.NewBuffer(nil)
|
||||
if data != nil {
|
||||
if err := json.NewEncoder(params).Encode(data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return params, nil
|
||||
}
|
||||
|
||||
func ensureReaderClosed(response *ServerResponse) {
|
||||
if response != nil && response.body != nil {
|
||||
response.body.Close()
|
||||
}
|
||||
}
|
|
@ -192,7 +192,14 @@ func Load(configDir string) (*ConfigFile, error) {
|
|||
}
|
||||
defer file.Close()
|
||||
err = configFile.LegacyLoadFromReader(file)
|
||||
return &configFile, err
|
||||
if err != nil {
|
||||
return &configFile, err
|
||||
}
|
||||
|
||||
if configFile.HTTPHeaders == nil {
|
||||
configFile.HTTPHeaders = map[string]string{}
|
||||
}
|
||||
return &configFile, nil
|
||||
}
|
||||
|
||||
// SaveToWriter encodes and writes out all the authorization information to
|
||||
|
|
Loading…
Reference in a new issue