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 }