github.com/demonoid81/moby@v0.0.0-20200517203328-62dd8e17c460/pkg/plugins/client.go (about)

     1  package plugins // import "github.com/demonoid81/moby/pkg/plugins"
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"io"
     8  	"io/ioutil"
     9  	"net/http"
    10  	"net/url"
    11  	"time"
    12  
    13  	"github.com/demonoid81/moby/pkg/ioutils"
    14  	"github.com/demonoid81/moby/pkg/plugins/transport"
    15  	"github.com/docker/go-connections/sockets"
    16  	"github.com/docker/go-connections/tlsconfig"
    17  	"github.com/sirupsen/logrus"
    18  )
    19  
    20  const (
    21  	defaultTimeOut = 30
    22  )
    23  
    24  func newTransport(addr string, tlsConfig *tlsconfig.Options) (transport.Transport, error) {
    25  	tr := &http.Transport{}
    26  
    27  	if tlsConfig != nil {
    28  		c, err := tlsconfig.Client(*tlsConfig)
    29  		if err != nil {
    30  			return nil, err
    31  		}
    32  		tr.TLSClientConfig = c
    33  	}
    34  
    35  	u, err := url.Parse(addr)
    36  	if err != nil {
    37  		return nil, err
    38  	}
    39  	socket := u.Host
    40  	if socket == "" {
    41  		// valid local socket addresses have the host empty.
    42  		socket = u.Path
    43  	}
    44  	if err := sockets.ConfigureTransport(tr, u.Scheme, socket); err != nil {
    45  		return nil, err
    46  	}
    47  	scheme := httpScheme(u)
    48  
    49  	return transport.NewHTTPTransport(tr, scheme, socket), nil
    50  }
    51  
    52  // NewClient creates a new plugin client (http).
    53  func NewClient(addr string, tlsConfig *tlsconfig.Options) (*Client, error) {
    54  	clientTransport, err := newTransport(addr, tlsConfig)
    55  	if err != nil {
    56  		return nil, err
    57  	}
    58  	return newClientWithTransport(clientTransport, 0), nil
    59  }
    60  
    61  // NewClientWithTimeout creates a new plugin client (http).
    62  func NewClientWithTimeout(addr string, tlsConfig *tlsconfig.Options, timeout time.Duration) (*Client, error) {
    63  	clientTransport, err := newTransport(addr, tlsConfig)
    64  	if err != nil {
    65  		return nil, err
    66  	}
    67  	return newClientWithTransport(clientTransport, timeout), nil
    68  }
    69  
    70  // newClientWithTransport creates a new plugin client with a given transport.
    71  func newClientWithTransport(tr transport.Transport, timeout time.Duration) *Client {
    72  	return &Client{
    73  		http: &http.Client{
    74  			Transport: tr,
    75  			Timeout:   timeout,
    76  		},
    77  		requestFactory: tr,
    78  	}
    79  }
    80  
    81  // Client represents a plugin client.
    82  type Client struct {
    83  	http           *http.Client // http client to use
    84  	requestFactory transport.RequestFactory
    85  }
    86  
    87  // RequestOpts is the set of options that can be passed into a request
    88  type RequestOpts struct {
    89  	Timeout time.Duration
    90  }
    91  
    92  // WithRequestTimeout sets a timeout duration for plugin requests
    93  func WithRequestTimeout(t time.Duration) func(*RequestOpts) {
    94  	return func(o *RequestOpts) {
    95  		o.Timeout = t
    96  	}
    97  }
    98  
    99  // Call calls the specified method with the specified arguments for the plugin.
   100  // It will retry for 30 seconds if a failure occurs when calling.
   101  func (c *Client) Call(serviceMethod string, args, ret interface{}) error {
   102  	return c.CallWithOptions(serviceMethod, args, ret)
   103  }
   104  
   105  // CallWithOptions is just like call except it takes options
   106  func (c *Client) CallWithOptions(serviceMethod string, args interface{}, ret interface{}, opts ...func(*RequestOpts)) error {
   107  	var buf bytes.Buffer
   108  	if args != nil {
   109  		if err := json.NewEncoder(&buf).Encode(args); err != nil {
   110  			return err
   111  		}
   112  	}
   113  	body, err := c.callWithRetry(serviceMethod, &buf, true, opts...)
   114  	if err != nil {
   115  		return err
   116  	}
   117  	defer body.Close()
   118  	if ret != nil {
   119  		if err := json.NewDecoder(body).Decode(&ret); err != nil {
   120  			logrus.Errorf("%s: error reading plugin resp: %v", serviceMethod, err)
   121  			return err
   122  		}
   123  	}
   124  	return nil
   125  }
   126  
   127  // Stream calls the specified method with the specified arguments for the plugin and returns the response body
   128  func (c *Client) Stream(serviceMethod string, args interface{}) (io.ReadCloser, error) {
   129  	var buf bytes.Buffer
   130  	if err := json.NewEncoder(&buf).Encode(args); err != nil {
   131  		return nil, err
   132  	}
   133  	return c.callWithRetry(serviceMethod, &buf, true)
   134  }
   135  
   136  // SendFile calls the specified method, and passes through the IO stream
   137  func (c *Client) SendFile(serviceMethod string, data io.Reader, ret interface{}) error {
   138  	body, err := c.callWithRetry(serviceMethod, data, true)
   139  	if err != nil {
   140  		return err
   141  	}
   142  	defer body.Close()
   143  	if err := json.NewDecoder(body).Decode(&ret); err != nil {
   144  		logrus.Errorf("%s: error reading plugin resp: %v", serviceMethod, err)
   145  		return err
   146  	}
   147  	return nil
   148  }
   149  
   150  func (c *Client) callWithRetry(serviceMethod string, data io.Reader, retry bool, reqOpts ...func(*RequestOpts)) (io.ReadCloser, error) {
   151  	var retries int
   152  	start := time.Now()
   153  
   154  	var opts RequestOpts
   155  	for _, o := range reqOpts {
   156  		o(&opts)
   157  	}
   158  
   159  	for {
   160  		req, err := c.requestFactory.NewRequest(serviceMethod, data)
   161  		if err != nil {
   162  			return nil, err
   163  		}
   164  
   165  		cancelRequest := func() {}
   166  		if opts.Timeout > 0 {
   167  			var ctx context.Context
   168  			ctx, cancelRequest = context.WithTimeout(req.Context(), opts.Timeout)
   169  			req = req.WithContext(ctx)
   170  		}
   171  
   172  		resp, err := c.http.Do(req)
   173  		if err != nil {
   174  			cancelRequest()
   175  			if !retry {
   176  				return nil, err
   177  			}
   178  
   179  			timeOff := backoff(retries)
   180  			if abort(start, timeOff) {
   181  				return nil, err
   182  			}
   183  			retries++
   184  			logrus.Warnf("Unable to connect to plugin: %s%s: %v, retrying in %v", req.URL.Host, req.URL.Path, err, timeOff)
   185  			time.Sleep(timeOff)
   186  			continue
   187  		}
   188  
   189  		if resp.StatusCode != http.StatusOK {
   190  			b, err := ioutil.ReadAll(resp.Body)
   191  			resp.Body.Close()
   192  			cancelRequest()
   193  			if err != nil {
   194  				return nil, &statusError{resp.StatusCode, serviceMethod, err.Error()}
   195  			}
   196  
   197  			// Plugins' Response(s) should have an Err field indicating what went
   198  			// wrong. Try to unmarshal into ResponseErr. Otherwise fallback to just
   199  			// return the string(body)
   200  			type responseErr struct {
   201  				Err string
   202  			}
   203  			remoteErr := responseErr{}
   204  			if err := json.Unmarshal(b, &remoteErr); err == nil {
   205  				if remoteErr.Err != "" {
   206  					return nil, &statusError{resp.StatusCode, serviceMethod, remoteErr.Err}
   207  				}
   208  			}
   209  			// old way...
   210  			return nil, &statusError{resp.StatusCode, serviceMethod, string(b)}
   211  		}
   212  		return ioutils.NewReadCloserWrapper(resp.Body, func() error {
   213  			err := resp.Body.Close()
   214  			cancelRequest()
   215  			return err
   216  		}), nil
   217  	}
   218  }
   219  
   220  func backoff(retries int) time.Duration {
   221  	b, max := 1, defaultTimeOut
   222  	for b < max && retries > 0 {
   223  		b *= 2
   224  		retries--
   225  	}
   226  	if b > max {
   227  		b = max
   228  	}
   229  	return time.Duration(b) * time.Second
   230  }
   231  
   232  func abort(start time.Time, timeOff time.Duration) bool {
   233  	return timeOff+time.Since(start) >= time.Duration(defaultTimeOut)*time.Second
   234  }
   235  
   236  func httpScheme(u *url.URL) string {
   237  	scheme := u.Scheme
   238  	if scheme != "https" {
   239  		scheme = "http"
   240  	}
   241  	return scheme
   242  }