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