client.go 8.9 KB

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