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