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 }