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