api.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. // Copyright Joyent, Inc.
  2. //
  3. // This Source Code Form is subject to the terms of the Mozilla Public
  4. // License, v. 2.0. If a copy of the MPL was not distributed with this
  5. // file, You can obtain one at http://mozilla.org/MPL/2.0/.
  6. package main
  7. import (
  8. "encoding/json"
  9. "errors"
  10. "fmt"
  11. "io/ioutil"
  12. "net"
  13. "net/http"
  14. "strings"
  15. "time"
  16. "github.com/dghubble/sling"
  17. )
  18. type Conch struct {
  19. URL string
  20. Token string
  21. UserAgent map[string]string
  22. Debug bool
  23. Trace bool
  24. JsonOnly bool
  25. StrictParsing bool
  26. DevelMode bool
  27. HTTP *http.Client
  28. }
  29. var defaultTransport = &http.Transport{
  30. Proxy: http.ProxyFromEnvironment,
  31. Dial: (&net.Dialer{
  32. Timeout: 5 * time.Second,
  33. KeepAlive: 5 * time.Second,
  34. DualStack: true,
  35. }).Dial,
  36. TLSHandshakeTimeout: 5 * time.Second,
  37. }
  38. func (c *Conch) PrintJSON(d interface{}) {
  39. fmt.Println(c.AsJSON(d))
  40. }
  41. func (c *Conch) AsJSON(d interface{}) string {
  42. if j, err := json.MarshalIndent(d, "", " "); err != nil {
  43. panic(err)
  44. } else {
  45. return string(j)
  46. }
  47. }
  48. func (c *Conch) Sling() *sling.Sling {
  49. userAgent := fmt.Sprintf("Conch/%s", Version)
  50. if len(c.UserAgent) > 0 {
  51. for k, v := range c.UserAgent {
  52. userAgent = fmt.Sprintf("%s %s/%s", userAgent, k, v)
  53. }
  54. }
  55. if c.HTTP == nil {
  56. c.HTTP = &http.Client{
  57. Transport: defaultTransport,
  58. }
  59. }
  60. s := sling.New().
  61. Client(c.HTTP).
  62. Set("User-Agent", userAgent)
  63. if c.URL != "" {
  64. s = s.Base(c.URL)
  65. }
  66. if c.Token != "" {
  67. s = s.Set("Authorization", "Bearer "+c.Token)
  68. }
  69. return s.New()
  70. }
  71. func (c *Conch) DoBadly(s *sling.Sling) *ConchResponse {
  72. response := ConchResponse{Strict: c.StrictParsing}
  73. req, err := s.Request()
  74. if err != nil {
  75. response.Error = err
  76. panic(&response)
  77. }
  78. response.Request = req
  79. // BUG(sungo) Logging
  80. res, err := c.HTTP.Do(req)
  81. response.Response = res
  82. if (res == nil) || (err != nil) {
  83. response.Error = err
  84. panic(&response)
  85. }
  86. defer res.Body.Close()
  87. bodyBytes, err := ioutil.ReadAll(res.Body)
  88. if err != nil {
  89. response.Error = err
  90. panic(&response)
  91. }
  92. response.Body = string(bodyBytes)
  93. return &response
  94. }
  95. func (c *Conch) Do(s *sling.Sling) *ConchResponse {
  96. response := ConchResponse{Strict: c.StrictParsing}
  97. req, err := s.Request()
  98. if err != nil {
  99. response.Error = err
  100. panic(&response)
  101. }
  102. response.Request = req
  103. // BUG(sungo) Logging
  104. res, err := c.HTTP.Do(req)
  105. response.Response = res
  106. if (res == nil) || (err != nil) {
  107. response.Error = err
  108. panic(&response)
  109. }
  110. defer res.Body.Close()
  111. bodyBytes, err := ioutil.ReadAll(res.Body)
  112. if err != nil {
  113. response.Error = err
  114. panic(&response)
  115. }
  116. response.Body = string(bodyBytes)
  117. if response.IsError() {
  118. e := struct {
  119. Error string `json:"error"`
  120. }{}
  121. if ok := response.Parse(&e); ok {
  122. response.Error = fmt.Errorf(
  123. "HTTP Error %d: %s",
  124. response.StatusCode(),
  125. e.Error,
  126. )
  127. } else {
  128. response.Error = fmt.Errorf(
  129. "HTTP Error %d: %s",
  130. response.StatusCode(),
  131. response.Status(),
  132. )
  133. }
  134. panic(&response)
  135. }
  136. switch response.StatusCode() {
  137. case 201:
  138. fallthrough
  139. case 204:
  140. if location, err := response.Response.Location(); err == nil {
  141. if location != nil {
  142. return c.Do(c.Sling().Get(location.String()))
  143. }
  144. }
  145. }
  146. return &response
  147. }
  148. func (c *Conch) Version() string {
  149. res := c.Do(c.Sling().Get("/version"))
  150. v := struct {
  151. Version string `json:"version"`
  152. }{}
  153. if ok := res.Parse(&v); !ok {
  154. panic(res)
  155. }
  156. return v.Version
  157. }
  158. /*****************/
  159. // ErrNoResponse indicates an operation was attempted on the structure when no
  160. // HTTP response is present
  161. var ErrNoResponse = errors.New("no HTTP response found")
  162. // ConchResponse holds the notions of what happened to an HTTP request and
  163. // convenience functions around payload parsing and the like
  164. type ConchResponse struct {
  165. Request *http.Request
  166. Response *http.Response
  167. Strict bool
  168. Body string
  169. Error error
  170. }
  171. /***/
  172. // StatusCode provides the HTTP response status code. If we don't have a
  173. // response, -1 is returned.
  174. func (r *ConchResponse) StatusCode() int {
  175. if r.Response == nil {
  176. return -1
  177. }
  178. return r.Response.StatusCode
  179. }
  180. // Status provides the string version of the HTTP status code. If we don't have
  181. // a response, "" is returned.
  182. func (r *ConchResponse) Status() string {
  183. if r.Response == nil {
  184. return ""
  185. }
  186. return r.Response.Status
  187. }
  188. // IsError provides a really simplistic notion of when an HTTP response has
  189. // gone awry. Specifically, if the status code is between 400 and 600, it is
  190. // considered in error. The response is also considered to be ok/successful if
  191. // it hasn't happened yet.
  192. func (r *ConchResponse) IsError() bool {
  193. if r.Response == nil {
  194. return false
  195. }
  196. if (r.StatusCode() >= 400) && (r.StatusCode() < 600) {
  197. return true
  198. }
  199. return false
  200. }
  201. // IsErrorOurFault is a convenience function to spot when an HTTP error code is
  202. // in the 400s
  203. func (r *ConchResponse) IsErrorOurFault() bool {
  204. if !r.IsError() {
  205. return false
  206. }
  207. if r.StatusCode() >= 400 && r.StatusCode() < 500 {
  208. return true
  209. }
  210. return false
  211. }
  212. // IsErrorTheirFault is a convenience function to spot when an HTTP error code
  213. // is in the 500s
  214. func (r *ConchResponse) IsErrorTheirFault() bool {
  215. if !r.IsError() {
  216. return false
  217. }
  218. if r.StatusCode() >= 500 && r.StatusCode() < 600 {
  219. return true
  220. }
  221. return false
  222. }
  223. // Parse, well, parses the JSON payload in the HTTP response and tries to shove
  224. // it into the provided structure. If Strict is true, the parser disallows
  225. // any unknown fields. To quote the go docs, an error is returned when "the
  226. // input contains object keys which do not match any non-ignored, exported
  227. // fields in the destination."
  228. //
  229. // So, if the API sends us data we aren't expecting, you'll get an error.
  230. // However, you will *not* get an error if the API fails to send data you're
  231. // expecting.
  232. func (r *ConchResponse) Parse(data interface{}) bool {
  233. r.Error = nil
  234. if r.Response == nil {
  235. r.Error = ErrNoResponse
  236. return false
  237. }
  238. dec := json.NewDecoder(strings.NewReader(r.Body))
  239. if r.Strict {
  240. dec.DisallowUnknownFields()
  241. }
  242. err := dec.Decode(data)
  243. r.Error = err
  244. return (err == nil)
  245. }