| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348 |
- /*
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
- */
- package client
- import (
- "bytes"
- "crypto/tls"
- "encoding/json"
- "fmt"
- "io"
- "net/http"
- "net/url"
- "strings"
- "time"
- )
- type DopplerClient struct {
- baseURL *url.URL
- DopplerToken string
- VerifyTLS bool
- UserAgent string
- }
- type queryParams map[string]string
- type headers map[string]string
- type httpRequestBody []byte
- type Secrets map[string]string
- type RawSecrets map[string]*interface{}
- type APIError struct {
- Err error
- Message string
- Data string
- }
- type apiResponse struct {
- HTTPResponse *http.Response
- Body []byte
- }
- type apiErrorResponse struct {
- Messages []string
- Success bool
- }
- type SecretRequest struct {
- Name string
- Project string
- Config string
- }
- type SecretsRequest struct {
- Project string
- Config string
- NameTransformer string
- Format string
- ETag string // Specifying an ETag implies that the caller has implemented response caching
- }
- type UpdateSecretsRequest struct {
- Secrets RawSecrets `json:"secrets,omitempty"`
- Project string `json:"project,omitempty"`
- Config string `json:"config,omitempty"`
- }
- type secretResponseBody struct {
- Name string `json:"name,omitempty"`
- Value struct {
- Raw *string `json:"raw"`
- Computed *string `json:"computed"`
- } `json:"value,omitempty"`
- Messages *[]string `json:"messages,omitempty"`
- Success bool `json:"success"`
- }
- type SecretResponse struct {
- Name string
- Value string
- }
- type SecretsResponse struct {
- Secrets Secrets
- Body []byte
- Modified bool
- ETag string
- }
- func NewDopplerClient(dopplerToken string) (*DopplerClient, error) {
- client := &DopplerClient{
- DopplerToken: dopplerToken,
- VerifyTLS: true,
- UserAgent: "doppler-external-secrets",
- }
- if err := client.SetBaseURL("https://api.doppler.com"); err != nil {
- return nil, &APIError{Err: err, Message: "setting base URL failed"}
- }
- return client, nil
- }
- func (c *DopplerClient) BaseURL() *url.URL {
- u := *c.baseURL
- return &u
- }
- func (c *DopplerClient) SetBaseURL(urlStr string) error {
- baseURL, err := url.Parse(strings.TrimSuffix(urlStr, "/"))
- if err != nil {
- return err
- }
- if baseURL.Scheme == "" {
- baseURL.Scheme = "https"
- }
- c.baseURL = baseURL
- return nil
- }
- func (c *DopplerClient) Authenticate() error {
- // Choose projects as a lightweight endpoint for testing authentication
- if _, err := c.performRequest("/v3/projects", "GET", headers{}, queryParams{}, httpRequestBody{}); err != nil {
- return err
- }
- return nil
- }
- func (c *DopplerClient) GetSecret(request SecretRequest) (*SecretResponse, error) {
- params := request.buildQueryParams(request.Name)
- response, err := c.performRequest("/v3/configs/config/secret", "GET", headers{}, params, httpRequestBody{})
- if err != nil {
- return nil, err
- }
- var data secretResponseBody
- if err := json.Unmarshal(response.Body, &data); err != nil {
- return nil, &APIError{Err: err, Message: "unable to unmarshal secret payload", Data: string(response.Body)}
- }
- if data.Value.Computed == nil {
- return nil, &APIError{Message: fmt.Sprintf("secret '%s' not found", request.Name)}
- }
- return &SecretResponse{Name: data.Name, Value: *data.Value.Computed}, nil
- }
- // GetSecrets should only have an ETag supplied if Secrets are cached as SecretsResponse.Secrets will be nil if 304 (not modified) returned.
- func (c *DopplerClient) GetSecrets(request SecretsRequest) (*SecretsResponse, error) {
- headers := headers{}
- if request.ETag != "" {
- headers["if-none-match"] = request.ETag
- }
- if request.Format != "" && request.Format != "json" {
- headers["accept"] = "text/plain"
- }
- params := request.buildQueryParams()
- response, apiErr := c.performRequest("/v3/configs/config/secrets/download", "GET", headers, params, httpRequestBody{})
- if apiErr != nil {
- return nil, apiErr
- }
- if response.HTTPResponse.StatusCode == 304 {
- return &SecretsResponse{Modified: false, Secrets: nil, ETag: request.ETag}, nil
- }
- eTag := response.HTTPResponse.Header.Get("etag")
- // Format defeats JSON parsing
- if request.Format != "" {
- return &SecretsResponse{Modified: true, Body: response.Body, ETag: eTag}, nil
- }
- var secrets Secrets
- if err := json.Unmarshal(response.Body, &secrets); err != nil {
- return nil, &APIError{Err: err, Message: "unable to unmarshal secrets payload"}
- }
- return &SecretsResponse{Modified: true, Secrets: secrets, Body: response.Body, ETag: eTag}, nil
- }
- func (c *DopplerClient) UpdateSecrets(request UpdateSecretsRequest) error {
- body, jsonErr := json.Marshal(request)
- if jsonErr != nil {
- return &APIError{Err: jsonErr, Message: "unable to unmarshal update secrets payload"}
- }
- _, err := c.performRequest("/v3/configs/config/secrets", "POST", headers{}, queryParams{}, body)
- if err != nil {
- return err
- }
- return nil
- }
- func (r *SecretRequest) buildQueryParams(name string) queryParams {
- params := queryParams{}
- params["name"] = name
- if r.Project != "" {
- params["project"] = r.Project
- }
- if r.Config != "" {
- params["config"] = r.Config
- }
- return params
- }
- func (r *SecretsRequest) buildQueryParams() queryParams {
- params := queryParams{}
- if r.Project != "" {
- params["project"] = r.Project
- }
- if r.Config != "" {
- params["config"] = r.Config
- }
- if r.NameTransformer != "" {
- params["name_transformer"] = r.NameTransformer
- }
- if r.Format != "" {
- params["format"] = r.Format
- }
- return params
- }
- func (c *DopplerClient) performRequest(path, method string, headers headers, params queryParams, body httpRequestBody) (*apiResponse, error) {
- urlStr := c.BaseURL().String() + path
- reqURL, err := url.Parse(urlStr)
- if err != nil {
- return nil, &APIError{Err: err, Message: fmt.Sprintf("invalid API URL: %s", urlStr)}
- }
- var bodyReader io.Reader
- if body != nil {
- bodyReader = bytes.NewReader(body)
- } else {
- bodyReader = http.NoBody
- }
- req, err := http.NewRequest(method, reqURL.String(), bodyReader)
- if err != nil {
- return nil, &APIError{Err: err, Message: "unable to form HTTP request"}
- }
- if method == "POST" && req.Header.Get("content-type") == "" {
- req.Header.Set("content-type", "application/json")
- }
- if req.Header.Get("accept") == "" {
- req.Header.Set("accept", "application/json")
- }
- req.Header.Set("user-agent", c.UserAgent)
- req.SetBasicAuth(c.DopplerToken, "")
- for key, value := range headers {
- req.Header.Set(key, value)
- }
- query := req.URL.Query()
- for key, value := range params {
- query.Add(key, value)
- }
- req.URL.RawQuery = query.Encode()
- httpClient := &http.Client{Timeout: 10 * time.Second}
- tlsConfig := &tls.Config{
- MinVersion: tls.VersionTLS12,
- }
- if !c.VerifyTLS {
- tlsConfig.InsecureSkipVerify = true
- }
- httpClient.Transport = &http.Transport{
- DisableKeepAlives: true,
- TLSClientConfig: tlsConfig,
- }
- r, err := httpClient.Do(req)
- if err != nil {
- return nil, &APIError{Err: err, Message: "unable to load response"}
- }
- defer r.Body.Close()
- bodyResponse, err := io.ReadAll(r.Body)
- if err != nil {
- return &apiResponse{HTTPResponse: r, Body: nil}, &APIError{Err: err, Message: "unable to read entire response body"}
- }
- response := &apiResponse{HTTPResponse: r, Body: bodyResponse}
- success := isSuccess(r.StatusCode)
- if !success {
- if contentType := r.Header.Get("content-type"); strings.HasPrefix(contentType, "application/json") {
- var errResponse apiErrorResponse
- err := json.Unmarshal(bodyResponse, &errResponse)
- if err != nil {
- return response, &APIError{Err: err, Message: "unable to unmarshal error JSON payload"}
- }
- return response, &APIError{Err: nil, Message: strings.Join(errResponse.Messages, "\n")}
- }
- return nil, &APIError{Err: fmt.Errorf("%d status code; %d bytes", r.StatusCode, len(bodyResponse)), Message: "unable to load response"}
- }
- if success && err != nil {
- return nil, &APIError{Err: err, Message: "unable to load data from successful response"}
- }
- return response, nil
- }
- func isSuccess(statusCode int) bool {
- return (statusCode >= 200 && statusCode <= 299) || (statusCode >= 300 && statusCode <= 399)
- }
- func (e *APIError) Error() string {
- message := fmt.Sprintf("Doppler API Client Error: %s", e.Message)
- if underlyingError := e.Err; underlyingError != nil {
- message = fmt.Sprintf("%s\n%s", message, underlyingError.Error())
- }
- if e.Data != "" {
- message = fmt.Sprintf("%s\nData: %s", message, e.Data)
- }
- return message
- }
|