client.go 11 KB

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