client.go 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. /*
  2. Licensed under the Apache License, Version 2.0 (the "License");
  3. you may not use this file except in compliance with the License.
  4. You may obtain a copy of the License at
  5. http://www.apache.org/licenses/LICENSE-2.0
  6. Unless required by applicable law or agreed to in writing, software
  7. distributed under the License is distributed on an "AS IS" BASIS,
  8. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  9. See the License for the specific language governing permissions and
  10. limitations under the License.
  11. */
  12. package client
  13. import (
  14. "bytes"
  15. "crypto/tls"
  16. "encoding/json"
  17. "fmt"
  18. "io"
  19. "net/http"
  20. "net/url"
  21. "strings"
  22. "time"
  23. )
  24. type DopplerClient struct {
  25. baseURL *url.URL
  26. DopplerToken string
  27. VerifyTLS bool
  28. UserAgent string
  29. }
  30. type queryParams map[string]string
  31. type headers map[string]string
  32. type httpRequestBody []byte
  33. type Secrets map[string]string
  34. type RawSecrets map[string]*interface{}
  35. type APIError struct {
  36. Err error
  37. Message string
  38. Data string
  39. }
  40. type apiResponse struct {
  41. HTTPResponse *http.Response
  42. Body []byte
  43. }
  44. type apiErrorResponse struct {
  45. Messages []string
  46. Success bool
  47. }
  48. type SecretRequest struct {
  49. Name string
  50. Project string
  51. Config string
  52. }
  53. type SecretsRequest struct {
  54. Project string
  55. Config string
  56. NameTransformer string
  57. Format string
  58. ETag string // Specifying an ETag implies that the caller has implemented response caching
  59. }
  60. type UpdateSecretsRequest struct {
  61. Secrets RawSecrets `json:"secrets,omitempty"`
  62. Project string `json:"project,omitempty"`
  63. Config string `json:"config,omitempty"`
  64. }
  65. type secretResponseBody struct {
  66. Name string `json:"name,omitempty"`
  67. Value struct {
  68. Raw *string `json:"raw"`
  69. Computed *string `json:"computed"`
  70. } `json:"value,omitempty"`
  71. Messages *[]string `json:"messages,omitempty"`
  72. Success bool `json:"success"`
  73. }
  74. type SecretResponse struct {
  75. Name string
  76. Value string
  77. }
  78. type SecretsResponse struct {
  79. Secrets Secrets
  80. Body []byte
  81. Modified bool
  82. ETag string
  83. }
  84. func NewDopplerClient(dopplerToken string) (*DopplerClient, error) {
  85. client := &DopplerClient{
  86. DopplerToken: dopplerToken,
  87. VerifyTLS: true,
  88. UserAgent: "doppler-external-secrets",
  89. }
  90. if err := client.SetBaseURL("https://api.doppler.com"); err != nil {
  91. return nil, &APIError{Err: err, Message: "setting base URL failed"}
  92. }
  93. return client, nil
  94. }
  95. func (c *DopplerClient) BaseURL() *url.URL {
  96. u := *c.baseURL
  97. return &u
  98. }
  99. func (c *DopplerClient) SetBaseURL(urlStr string) error {
  100. baseURL, err := url.Parse(strings.TrimSuffix(urlStr, "/"))
  101. if err != nil {
  102. return err
  103. }
  104. if baseURL.Scheme == "" {
  105. baseURL.Scheme = "https"
  106. }
  107. c.baseURL = baseURL
  108. return nil
  109. }
  110. func (c *DopplerClient) Authenticate() error {
  111. // Choose projects as a lightweight endpoint for testing authentication
  112. if _, err := c.performRequest("/v3/projects", "GET", headers{}, queryParams{}, httpRequestBody{}); err != nil {
  113. return err
  114. }
  115. return nil
  116. }
  117. func (c *DopplerClient) GetSecret(request SecretRequest) (*SecretResponse, error) {
  118. params := request.buildQueryParams(request.Name)
  119. response, err := c.performRequest("/v3/configs/config/secret", "GET", headers{}, params, httpRequestBody{})
  120. if err != nil {
  121. return nil, err
  122. }
  123. var data secretResponseBody
  124. if err := json.Unmarshal(response.Body, &data); err != nil {
  125. return nil, &APIError{Err: err, Message: "unable to unmarshal secret payload", Data: string(response.Body)}
  126. }
  127. if data.Value.Computed == nil {
  128. return nil, &APIError{Message: fmt.Sprintf("secret '%s' not found", request.Name)}
  129. }
  130. return &SecretResponse{Name: data.Name, Value: *data.Value.Computed}, nil
  131. }
  132. // GetSecrets should only have an ETag supplied if Secrets are cached as SecretsResponse.Secrets will be nil if 304 (not modified) returned.
  133. func (c *DopplerClient) GetSecrets(request SecretsRequest) (*SecretsResponse, error) {
  134. headers := headers{}
  135. if request.ETag != "" {
  136. headers["if-none-match"] = request.ETag
  137. }
  138. if request.Format != "" && request.Format != "json" {
  139. headers["accept"] = "text/plain"
  140. }
  141. params := request.buildQueryParams()
  142. response, apiErr := c.performRequest("/v3/configs/config/secrets/download", "GET", headers, params, httpRequestBody{})
  143. if apiErr != nil {
  144. return nil, apiErr
  145. }
  146. if response.HTTPResponse.StatusCode == 304 {
  147. return &SecretsResponse{Modified: false, Secrets: nil, ETag: request.ETag}, nil
  148. }
  149. eTag := response.HTTPResponse.Header.Get("etag")
  150. // Format defeats JSON parsing
  151. if request.Format != "" {
  152. return &SecretsResponse{Modified: true, Body: response.Body, ETag: eTag}, nil
  153. }
  154. var secrets Secrets
  155. if err := json.Unmarshal(response.Body, &secrets); err != nil {
  156. return nil, &APIError{Err: err, Message: "unable to unmarshal secrets payload"}
  157. }
  158. return &SecretsResponse{Modified: true, Secrets: secrets, Body: response.Body, ETag: eTag}, nil
  159. }
  160. func (c *DopplerClient) UpdateSecrets(request UpdateSecretsRequest) error {
  161. body, jsonErr := json.Marshal(request)
  162. if jsonErr != nil {
  163. return &APIError{Err: jsonErr, Message: "unable to unmarshal update secrets payload"}
  164. }
  165. _, err := c.performRequest("/v3/configs/config/secrets", "POST", headers{}, queryParams{}, body)
  166. if err != nil {
  167. return err
  168. }
  169. return nil
  170. }
  171. func (r *SecretRequest) buildQueryParams(name string) queryParams {
  172. params := queryParams{}
  173. params["name"] = name
  174. if r.Project != "" {
  175. params["project"] = r.Project
  176. }
  177. if r.Config != "" {
  178. params["config"] = r.Config
  179. }
  180. return params
  181. }
  182. func (r *SecretsRequest) buildQueryParams() queryParams {
  183. params := queryParams{}
  184. if r.Project != "" {
  185. params["project"] = r.Project
  186. }
  187. if r.Config != "" {
  188. params["config"] = r.Config
  189. }
  190. if r.NameTransformer != "" {
  191. params["name_transformer"] = r.NameTransformer
  192. }
  193. if r.Format != "" {
  194. params["format"] = r.Format
  195. }
  196. return params
  197. }
  198. func (c *DopplerClient) performRequest(path, method string, headers headers, params queryParams, body httpRequestBody) (*apiResponse, error) {
  199. urlStr := c.BaseURL().String() + path
  200. reqURL, err := url.Parse(urlStr)
  201. if err != nil {
  202. return nil, &APIError{Err: err, Message: fmt.Sprintf("invalid API URL: %s", urlStr)}
  203. }
  204. var bodyReader io.Reader
  205. if body != nil {
  206. bodyReader = bytes.NewReader(body)
  207. } else {
  208. bodyReader = http.NoBody
  209. }
  210. req, err := http.NewRequest(method, reqURL.String(), bodyReader)
  211. if err != nil {
  212. return nil, &APIError{Err: err, Message: "unable to form HTTP request"}
  213. }
  214. if method == "POST" && req.Header.Get("content-type") == "" {
  215. req.Header.Set("content-type", "application/json")
  216. }
  217. if req.Header.Get("accept") == "" {
  218. req.Header.Set("accept", "application/json")
  219. }
  220. req.Header.Set("user-agent", c.UserAgent)
  221. req.SetBasicAuth(c.DopplerToken, "")
  222. for key, value := range headers {
  223. req.Header.Set(key, value)
  224. }
  225. query := req.URL.Query()
  226. for key, value := range params {
  227. query.Add(key, value)
  228. }
  229. req.URL.RawQuery = query.Encode()
  230. httpClient := &http.Client{Timeout: 10 * time.Second}
  231. tlsConfig := &tls.Config{
  232. MinVersion: tls.VersionTLS12,
  233. }
  234. if !c.VerifyTLS {
  235. tlsConfig.InsecureSkipVerify = true
  236. }
  237. httpClient.Transport = &http.Transport{
  238. DisableKeepAlives: true,
  239. TLSClientConfig: tlsConfig,
  240. }
  241. r, err := httpClient.Do(req)
  242. if err != nil {
  243. return nil, &APIError{Err: err, Message: "unable to load response"}
  244. }
  245. defer r.Body.Close()
  246. bodyResponse, err := io.ReadAll(r.Body)
  247. if err != nil {
  248. return &apiResponse{HTTPResponse: r, Body: nil}, &APIError{Err: err, Message: "unable to read entire response body"}
  249. }
  250. response := &apiResponse{HTTPResponse: r, Body: bodyResponse}
  251. success := isSuccess(r.StatusCode)
  252. if !success {
  253. if contentType := r.Header.Get("content-type"); strings.HasPrefix(contentType, "application/json") {
  254. var errResponse apiErrorResponse
  255. err := json.Unmarshal(bodyResponse, &errResponse)
  256. if err != nil {
  257. return response, &APIError{Err: err, Message: "unable to unmarshal error JSON payload"}
  258. }
  259. return response, &APIError{Err: nil, Message: strings.Join(errResponse.Messages, "\n")}
  260. }
  261. return nil, &APIError{Err: fmt.Errorf("%d status code; %d bytes", r.StatusCode, len(bodyResponse)), Message: "unable to load response"}
  262. }
  263. if success && err != nil {
  264. return nil, &APIError{Err: err, Message: "unable to load data from successful response"}
  265. }
  266. return response, nil
  267. }
  268. func isSuccess(statusCode int) bool {
  269. return (statusCode >= 200 && statusCode <= 299) || (statusCode >= 300 && statusCode <= 399)
  270. }
  271. func (e *APIError) Error() string {
  272. message := fmt.Sprintf("Doppler API Client Error: %s", e.Message)
  273. if underlyingError := e.Err; underlyingError != nil {
  274. message = fmt.Sprintf("%s\n%s", message, underlyingError.Error())
  275. }
  276. if e.Data != "" {
  277. message = fmt.Sprintf("%s\nData: %s", message, e.Data)
  278. }
  279. return message
  280. }