github.com/koron/hk@v0.0.0-20150303213137-b8aeaa3ab34c/postgresql/client.go (about)

     1  package postgresql
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"log"
     9  	"net/http"
    10  	"net/http/httputil"
    11  	"os"
    12  	"runtime"
    13  	"strings"
    14  
    15  	"github.com/heroku/hk/Godeps/_workspace/src/code.google.com/p/go-uuid/uuid"
    16  )
    17  
    18  const (
    19  	Version              = "0.0.1"
    20  	DefaultAPIPath       = "/client/v11/databases"
    21  	DefaultAPIURL        = "https://postgres-api.heroku.com" + DefaultAPIPath
    22  	DefaultStarterAPIURL = "https://postgres-starter-api.heroku.com" + DefaultAPIPath
    23  	DefaultUserAgent     = "heroku-postgres-go/" + Version + " (" + runtime.GOOS + "; " + runtime.GOARCH + ")"
    24  )
    25  
    26  // A Client is a Heroku Postgres API client. Its zero value is a usable client
    27  // that uses default settings for the Heroku Postgres API. The Client has an
    28  // internal HTTP client (HTTP) which defaults to http.DefaultClient.
    29  //
    30  // As with all http.Clients, this Client's Transport has internal state (cached
    31  // HTTP connections), so Clients should be reused instead of created as needed.
    32  // Clients are safe for use by multiple goroutines.
    33  type Client struct {
    34  	// HTTP is the Client's internal http.Client, handling HTTP requests to the
    35  	// Heroku Postgres API.
    36  	HTTP *http.Client
    37  
    38  	// The URL of the Heroku Postgres API to communicate with. Defaults to
    39  	// DefaultAPIURL.
    40  	URL string
    41  
    42  	// The URL of the Heroku Postgres Starter API to communicate with. Defaults
    43  	// to DefaultStarterAPIURL.
    44  	StarterURL string
    45  
    46  	// Username is the HTTP basic auth username for API calls made by this Client.
    47  	Username string
    48  
    49  	// Password is the HTTP basic auth password for API calls made by this Client.
    50  	Password string
    51  
    52  	// UserAgent to be provided in API requests. Set to DefaultUserAgent if not
    53  	// specified.
    54  	UserAgent string
    55  
    56  	// Debug mode can be used to dump the full request and response to stdout.
    57  	Debug bool
    58  
    59  	// AdditionalHeaders are extra headers to add to each HTTP request sent by
    60  	// this Client.
    61  	AdditionalHeaders http.Header
    62  
    63  	// Path to the Unix domain socket or a running heroku-agent.
    64  	HerokuAgentSocket string
    65  }
    66  
    67  func (c *Client) Get(isStarterPlan bool, path string, v interface{}) error {
    68  	return c.APIReq(isStarterPlan, "GET", path, v)
    69  }
    70  
    71  func (c *Client) Post(isStarterPlan bool, path string, v interface{}) error {
    72  	return c.APIReq(isStarterPlan, "POST", path, v)
    73  }
    74  
    75  func (c *Client) Put(isStarterPlan bool, path string, v interface{}) error {
    76  	return c.APIReq(isStarterPlan, "PUT", path, v)
    77  }
    78  
    79  // Creates a new DB struct initialized with this Client.
    80  func (c *Client) NewDB(id, plan string) DB {
    81  	return DB{
    82  		Id:     id,
    83  		Plan:   strings.TrimLeft(plan, "heroku-postgresql:"),
    84  		client: c,
    85  	}
    86  }
    87  
    88  // Generates an HTTP request for the Heroku Postgres API, but does not
    89  // perform the request. The request's Accept header field will be
    90  // set to:
    91  //
    92  //   Accept: application/json
    93  //
    94  // The Request-Id header will be set to a random UUID. The User-Agent header
    95  // will be set to the Client's UserAgent, or DefaultUserAgent if UserAgent is
    96  // not set.
    97  //
    98  // isStarterPlan should be set to true if the target database is a starter plan,
    99  // and false otherwise (as defined in DB.IsStarterPlan() ). Method is the HTTP
   100  // method of this request, and path is the HTTP path.
   101  func (c *Client) NewRequest(isStarterPlan bool, method, path string) (*http.Request, error) {
   102  	var rbody io.Reader
   103  
   104  	apiURL := strings.TrimRight(c.URL, "/")
   105  	if isStarterPlan {
   106  		apiURL = strings.TrimRight(c.StarterURL, "/")
   107  	}
   108  	if apiURL == "" {
   109  		if isStarterPlan {
   110  			apiURL = DefaultStarterAPIURL
   111  		} else {
   112  			apiURL = DefaultAPIURL
   113  		}
   114  	}
   115  	req, err := http.NewRequest(method, apiURL+path, rbody)
   116  	if err != nil {
   117  		return nil, err
   118  	}
   119  	// If we're talking to heroku-agent over a local Unix socket, downgrade to
   120  	// HTTP; heroku-agent will establish a secure connection between itself and
   121  	// the Heorku API.
   122  	if c.HerokuAgentSocket != "" {
   123  		req.URL.Scheme = "http"
   124  	}
   125  	req.Header.Set("Accept", "application/json")
   126  	req.Header.Set("Request-Id", uuid.New())
   127  	useragent := c.UserAgent
   128  	if useragent == "" {
   129  		useragent = DefaultUserAgent
   130  	}
   131  	req.Header.Set("User-Agent", useragent)
   132  	req.SetBasicAuth("", c.Password)
   133  	for k, v := range c.AdditionalHeaders {
   134  		req.Header[k] = v
   135  	}
   136  	return req, nil
   137  }
   138  
   139  // Sends a Heroku Postgres API request and decodes the response into v. As
   140  // described in DoReq(), the type of v determines how to handle the response
   141  // body.
   142  func (c *Client) APIReq(isStarterPlan bool, meth, path string, v interface{}) error {
   143  	req, err := c.NewRequest(isStarterPlan, meth, path)
   144  	if err != nil {
   145  		return err
   146  	}
   147  	return c.DoReq(req, v)
   148  }
   149  
   150  // Submits an HTTP request, checks its response, and deserializes
   151  // the response into v. The type of v determines how to handle
   152  // the response body:
   153  //
   154  //   nil        body is discarded
   155  //   io.Writer  body is copied directly into v
   156  //   else       body is decoded into v as json
   157  //
   158  func (c *Client) DoReq(req *http.Request, v interface{}) error {
   159  	if c.Debug {
   160  		dump, err := httputil.DumpRequestOut(req, true)
   161  		if err != nil {
   162  			log.Println(err)
   163  		} else {
   164  			os.Stderr.Write(dump)
   165  			os.Stderr.Write([]byte{'\n', '\n'})
   166  		}
   167  	}
   168  
   169  	httpClient := c.HTTP
   170  	if httpClient == nil {
   171  		httpClient = http.DefaultClient
   172  	}
   173  
   174  	res, err := httpClient.Do(req)
   175  	if err != nil {
   176  		return err
   177  	}
   178  	defer res.Body.Close()
   179  	if c.Debug {
   180  		dump, err := httputil.DumpResponse(res, true)
   181  		if err != nil {
   182  			log.Println(err)
   183  		} else {
   184  			os.Stderr.Write(dump)
   185  			os.Stderr.Write([]byte{'\n'})
   186  		}
   187  	}
   188  	if err = checkResp(res); err != nil {
   189  		return err
   190  	}
   191  	switch t := v.(type) {
   192  	case nil:
   193  	case io.Writer:
   194  		_, err = io.Copy(t, res.Body)
   195  	default:
   196  		err = json.NewDecoder(res.Body).Decode(v)
   197  	}
   198  	return err
   199  }
   200  
   201  func checkResp(res *http.Response) error {
   202  	if res.StatusCode/100 != 2 { // 200, 201, 202, etc
   203  		errb, err := ioutil.ReadAll(res.Body)
   204  		if err != nil {
   205  			return fmt.Errorf("unexpected error code=%d", res.StatusCode)
   206  		}
   207  		return fmt.Errorf("unexpected status code=%d message=%q", res.StatusCode, string(errb))
   208  	}
   209  	return nil
   210  }
   211  
   212  //   @headers = { :x_heroku_gem_version  => Heroku::Client.version }