client.go 9.8 KB

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