DockerCLI/vendor/github.com/docker/licensing/lib/go-clientlib/client.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
}
}
}