mirror of https://github.com/docker/cli.git
306 lines
8.6 KiB
Go
306 lines
8.6 KiB
Go
|
package clientlib
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"context"
|
||
|
"encoding/json"
|
||
|
"encoding/xml"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"io/ioutil"
|
||
|
"net/http"
|
||
|
"strings"
|
||
|
|
||
|
"github.com/docker/licensing/lib/errors"
|
||
|
)
|
||
|
|
||
|
// Do is a shortcut for creating and executing an http request.
|
||
|
func Do(ctx context.Context, method, urlStr string, opts ...RequestOption) (*http.Request, *http.Response, error) {
|
||
|
r, err := New(ctx, method, urlStr, opts...)
|
||
|
if err != nil {
|
||
|
return nil, nil, err
|
||
|
}
|
||
|
res, err := r.Do()
|
||
|
return r.Request, res, err
|
||
|
}
|
||
|
|
||
|
// New creates and returns a new Request, potentially configured via a vector of RequestOption's.
|
||
|
func New(ctx context.Context, method, urlStr string, opts ...RequestOption) (*Request, error) {
|
||
|
req, err := http.NewRequest(method, urlStr, nil)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
req = req.WithContext(ctx)
|
||
|
|
||
|
r := &Request{
|
||
|
Request: req,
|
||
|
Client: &http.Client{},
|
||
|
ErrorCheck: DefaultErrorCheck,
|
||
|
ErrorBodyMaxLength: defaultErrBodyMaxLength,
|
||
|
ErrorSummary: DefaultErrorSummary,
|
||
|
RequestPrepare: DefaultRequestPrepare,
|
||
|
ResponseHandle: DefaultResponseHandle,
|
||
|
}
|
||
|
|
||
|
for _, o := range opts {
|
||
|
o(r)
|
||
|
}
|
||
|
|
||
|
return r, nil
|
||
|
}
|
||
|
|
||
|
// Request encompasses an http.Request, plus configured behavior options.
|
||
|
type Request struct {
|
||
|
*http.Request
|
||
|
Client *http.Client
|
||
|
ErrorCheck ErrorCheck
|
||
|
ErrorBodyMaxLength int64
|
||
|
ErrorSummary ErrorSummary
|
||
|
ResponseHandle ResponseHandle
|
||
|
RequestPrepare RequestPrepare
|
||
|
}
|
||
|
|
||
|
// Do executes the Request. The Request.ErrorCheck to determine
|
||
|
// if this attempt has failed, and transform the returned error.
|
||
|
// Otherwise, Request.ResponseHandler will examine the response.
|
||
|
// It's expected that the ResponseHandler has been configured via
|
||
|
// a RequestOption to perform response parsing and storing.
|
||
|
func (r *Request) Do() (*http.Response, error) {
|
||
|
err := r.RequestPrepare(r)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
res, err := r.Client.Do(r.Request)
|
||
|
err = r.ErrorCheck(r, err, res)
|
||
|
if err != nil {
|
||
|
return res, err
|
||
|
}
|
||
|
return res, r.ResponseHandle(r, res)
|
||
|
}
|
||
|
|
||
|
// SetBody mirrors the ReadCloser config in http.NewRequest,
|
||
|
// ensuring that a ReadCloser is used for http.Request.Body.
|
||
|
func (r *Request) SetBody(body io.Reader) {
|
||
|
rc, ok := body.(io.ReadCloser)
|
||
|
if !ok && body != nil {
|
||
|
rc = ioutil.NopCloser(body)
|
||
|
}
|
||
|
r.Body = rc
|
||
|
}
|
||
|
|
||
|
// ErrorFields returns error annotation fields for the request.
|
||
|
func (r *Request) ErrorFields() map[string]interface{} {
|
||
|
return map[string]interface{}{
|
||
|
"url": r.URL.String(),
|
||
|
"method": r.Method,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// ErrorCheck is the signature for the function that is passed
|
||
|
// the error & response immediately from http.Client.Do().
|
||
|
type ErrorCheck func(r *Request, doErr error, res *http.Response) error
|
||
|
|
||
|
// DefaultErrorCheck is the default error checker used if none is
|
||
|
// configured on a Request. doErr and res are the return values of
|
||
|
// executing http.Client.Do(), so any implementation should first
|
||
|
// check doErr for non-nill & react appropriately. If an http response
|
||
|
// was received, if a non-200 class status was also received, then
|
||
|
// the response body will be read (up to a const limit) and passed
|
||
|
// to request.ErrorSummary to attempt to parse out the error body,
|
||
|
// which will be passed as the "detail" flag on the returned error.
|
||
|
func DefaultErrorCheck(r *Request, doErr error, res *http.Response) error {
|
||
|
if doErr != nil {
|
||
|
return errors.Wrap(doErr, r.ErrorFields())
|
||
|
}
|
||
|
status := res.StatusCode
|
||
|
if status >= 200 && status < 300 {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
defer res.Body.Close()
|
||
|
|
||
|
body, _ := ioutil.ReadAll(io.LimitReader(res.Body, r.ErrorBodyMaxLength))
|
||
|
detail := r.ErrorSummary(body)
|
||
|
|
||
|
message := fmt.Sprintf("%s %s returned %d : %s", r.Method, r.URL.String(), status, detail)
|
||
|
return errors.NewHTTPError(status, message).
|
||
|
With(r.ErrorFields()).
|
||
|
With(map[string]interface{}{
|
||
|
"http_status": status,
|
||
|
"detail": detail,
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// Default error response max length, in bytes
|
||
|
const defaultErrBodyMaxLength = 256
|
||
|
|
||
|
// ErrorSummary is the signature for the function that is passed
|
||
|
// the fully read body of an error response.
|
||
|
type ErrorSummary func([]byte) string
|
||
|
|
||
|
// DefaultErrorSummary just returns the string of the received
|
||
|
// error body. Note that the body passed in is potentially truncated
|
||
|
// before this call.
|
||
|
func DefaultErrorSummary(body []byte) string {
|
||
|
return string(body)
|
||
|
}
|
||
|
|
||
|
// RequestPrepare is the signature for the function called
|
||
|
// before calling http.Client.Do, to perform any preparation
|
||
|
// needed before executing the request, eg. marshaling the body.
|
||
|
type RequestPrepare func(r *Request) error
|
||
|
|
||
|
// DefaultRequestPrepare does nothing.
|
||
|
func DefaultRequestPrepare(*Request) error {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// ResponseHandle is the signature for the function called if
|
||
|
// ErrorCheck returns a nil error, and is responsible for performing
|
||
|
// any reads or stores from the request & response.
|
||
|
type ResponseHandle func(*Request, *http.Response) error
|
||
|
|
||
|
// DefaultResponseHandle merely closes the response body.
|
||
|
func DefaultResponseHandle(r *Request, res *http.Response) error {
|
||
|
res.Body.Close()
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// RequestOption is the signature for functions that can perform
|
||
|
// some configuration of a Request.
|
||
|
type RequestOption func(*Request)
|
||
|
|
||
|
// SendJSON returns a RequestOption that will marshal
|
||
|
// and set the json body & headers on a request.
|
||
|
func SendJSON(sends interface{}) RequestOption {
|
||
|
return func(r *Request) {
|
||
|
r.Header.Set("Content-Type", "application/json")
|
||
|
r.RequestPrepare = func(r *Request) error {
|
||
|
bits, err := json.Marshal(sends)
|
||
|
if err != nil {
|
||
|
return errors.Wrap(err, r.ErrorFields())
|
||
|
}
|
||
|
body := bytes.NewReader(bits)
|
||
|
r.SetBody(body)
|
||
|
r.ContentLength = int64(body.Len())
|
||
|
return nil
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// RecvJSON returns a RequestOption that will set the json headers
|
||
|
// on a request, and set a ResponseHandler that will unmarshal
|
||
|
// the response body to the given interface{}.
|
||
|
func RecvJSON(recvs interface{}) RequestOption {
|
||
|
return func(r *Request) {
|
||
|
r.Header.Set("Accept", "application/json")
|
||
|
r.Header.Set("Accept-Charset", "utf-8")
|
||
|
r.ResponseHandle = func(r *Request, res *http.Response) error {
|
||
|
defer res.Body.Close()
|
||
|
|
||
|
body, err := ioutil.ReadAll(res.Body)
|
||
|
if err != nil {
|
||
|
return errors.Wrap(err, r.ErrorFields())
|
||
|
}
|
||
|
|
||
|
err = json.Unmarshal(body, recvs)
|
||
|
if err != nil {
|
||
|
return errors.Wrap(err, r.ErrorFields())
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// SendXML returns a RequestOption that will marshal
|
||
|
// and set the xml body & headers on a request.
|
||
|
func SendXML(sends interface{}) RequestOption {
|
||
|
return func(r *Request) {
|
||
|
r.Header.Set("Content-Type", "application/xml")
|
||
|
r.RequestPrepare = func(r *Request) error {
|
||
|
bits, err := xml.Marshal(sends)
|
||
|
if err != nil {
|
||
|
return errors.Wrap(err, r.ErrorFields())
|
||
|
}
|
||
|
body := bytes.NewReader(bits)
|
||
|
r.SetBody(body)
|
||
|
r.ContentLength = int64(body.Len())
|
||
|
return nil
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// RecvXML returns a RequestOption that will set the xml headers
|
||
|
// on a request, and set a ResponseHandler that will unmarshal
|
||
|
// the response body to the given interface{}.
|
||
|
func RecvXML(recvs interface{}) RequestOption {
|
||
|
return func(r *Request) {
|
||
|
r.Header.Set("Accept", "application/xml")
|
||
|
r.Header.Set("Accept-Charset", "utf-8")
|
||
|
r.ResponseHandle = func(r *Request, res *http.Response) error {
|
||
|
defer res.Body.Close()
|
||
|
|
||
|
body, err := ioutil.ReadAll(res.Body)
|
||
|
if err != nil {
|
||
|
return errors.Wrap(err, r.ErrorFields())
|
||
|
}
|
||
|
|
||
|
err = xml.Unmarshal(body, recvs)
|
||
|
if err != nil {
|
||
|
return errors.Wrap(err, r.ErrorFields())
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// SendText returns a RequestOption that will marshal
|
||
|
// and set the text body & headers on a request.
|
||
|
func SendText(sends string) RequestOption {
|
||
|
return func(r *Request) {
|
||
|
r.Header.Set("Content-Type", "text/plain")
|
||
|
r.RequestPrepare = func(r *Request) error {
|
||
|
body := strings.NewReader(sends)
|
||
|
r.SetBody(body)
|
||
|
r.ContentLength = int64(body.Len())
|
||
|
return nil
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// RecvText returns a RequestOption that will set the text headers
|
||
|
// on a request, and set a ResponseHandler that will unmarshal
|
||
|
// the response body to the given string.
|
||
|
func RecvText(recvs *string) RequestOption {
|
||
|
return func(r *Request) {
|
||
|
r.Header.Set("Accept", "text/plain")
|
||
|
r.Header.Set("Accept-Charset", "utf-8")
|
||
|
r.ResponseHandle = func(r *Request, res *http.Response) error {
|
||
|
defer res.Body.Close()
|
||
|
|
||
|
body, err := ioutil.ReadAll(res.Body)
|
||
|
if err != nil {
|
||
|
return errors.Wrap(err, r.ErrorFields())
|
||
|
}
|
||
|
|
||
|
sbody := string(body)
|
||
|
*recvs = sbody
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// DontClose sets the ResponseBody to an empty function, so that the
|
||
|
// response body is not automatically closed. Users of this should be
|
||
|
// sure to call res.Body.Close().
|
||
|
func DontClose() RequestOption {
|
||
|
return func(r *Request) {
|
||
|
r.ResponseHandle = func(*Request, *http.Response) error {
|
||
|
return nil
|
||
|
}
|
||
|
}
|
||
|
}
|