client.go 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. /*
  2. Copyright © 2025 ESO Maintainer Team
  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 alibaba provides an implementation to interact with the Alibaba Cloud KMS and Secrets Manager.
  14. package alibaba
  15. import (
  16. "context"
  17. "errors"
  18. "fmt"
  19. "net/http"
  20. "net/url"
  21. "runtime"
  22. "strings"
  23. "time"
  24. openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
  25. kms "github.com/alibabacloud-go/kms-20160120/v3/client"
  26. openapiutil "github.com/alibabacloud-go/openapi-util/service"
  27. util "github.com/alibabacloud-go/tea-utils/v2/service"
  28. "github.com/alibabacloud-go/tea/tea"
  29. "github.com/hashicorp/go-retryablehttp"
  30. "github.com/external-secrets/external-secrets/runtime/esutils"
  31. )
  32. const (
  33. kmsAPIVersion = "2016-01-20"
  34. )
  35. // SecretsManagerClient defines the interface for interacting with the Alibaba Cloud Secrets Manager service.
  36. type SecretsManagerClient interface {
  37. GetSecretValue(
  38. ctx context.Context,
  39. request *kms.GetSecretValueRequest,
  40. ) (*kms.GetSecretValueResponseBody, error)
  41. Endpoint() string
  42. }
  43. type secretsManagerClient struct {
  44. config *openapi.Config
  45. options *util.RuntimeOptions
  46. endpoint string
  47. client *http.Client
  48. }
  49. var _ SecretsManagerClient = (*secretsManagerClient)(nil)
  50. func newClient(config *openapi.Config, options *util.RuntimeOptions) (*secretsManagerClient, error) {
  51. kmsClient, err := kms.NewClient(config)
  52. if err != nil {
  53. return nil, fmt.Errorf("failed to create Alibaba KMS client: %w", err)
  54. }
  55. endpoint, err := kmsClient.GetEndpoint(tea.String("kms"), kmsClient.RegionId, kmsClient.EndpointRule, kmsClient.Network, kmsClient.Suffix, kmsClient.EndpointMap, kmsClient.Endpoint)
  56. if err != nil {
  57. return nil, fmt.Errorf("failed to get KMS endpoint: %w", err)
  58. }
  59. if esutils.Deref(endpoint) == "" {
  60. return nil, errors.New("error KMS endpoint is missing")
  61. }
  62. const readWriteTimeoutSec = 60
  63. retryClient := retryablehttp.NewClient()
  64. retryClient.CheckRetry = retryablehttp.ErrorPropagatedRetryPolicy
  65. retryClient.Backoff = retryablehttp.DefaultBackoff
  66. retryClient.Logger = log
  67. retryClient.HTTPClient = &http.Client{
  68. Timeout: time.Second * time.Duration(readWriteTimeoutSec),
  69. }
  70. const defaultRetryAttempts = 3
  71. if esutils.Deref(options.Autoretry) {
  72. if options.MaxAttempts != nil {
  73. retryClient.RetryMax = esutils.Deref(options.MaxAttempts)
  74. } else {
  75. retryClient.RetryMax = defaultRetryAttempts
  76. }
  77. }
  78. return &secretsManagerClient{
  79. config: config,
  80. options: options,
  81. endpoint: esutils.Deref(endpoint),
  82. client: retryClient.StandardClient(),
  83. }, nil
  84. }
  85. func (s *secretsManagerClient) Endpoint() string {
  86. return s.endpoint
  87. }
  88. func (s *secretsManagerClient) GetSecretValue(
  89. ctx context.Context,
  90. request *kms.GetSecretValueRequest,
  91. ) (*kms.GetSecretValueResponseBody, error) {
  92. resp, err := s.doAPICall(ctx, "GetSecretValue", request)
  93. if err != nil {
  94. return nil, fmt.Errorf("error getting secret [%s] latest value: %w", esutils.Deref(request.SecretName), err)
  95. }
  96. body, err := esutils.ConvertToType[kms.GetSecretValueResponseBody](resp)
  97. if err != nil {
  98. return nil, fmt.Errorf("error converting body: %w", err)
  99. }
  100. return &body, nil
  101. }
  102. func (s *secretsManagerClient) doAPICall(ctx context.Context,
  103. action string,
  104. request any) (any, error) {
  105. creds, err := s.config.Credential.GetCredential()
  106. if err != nil {
  107. return nil, fmt.Errorf("could not get credentials: %w", err)
  108. }
  109. apiRequest := newOpenAPIRequest(s.endpoint, action, methodTypeGET, request)
  110. apiRequest.query["AccessKeyId"] = creds.AccessKeyId
  111. if esutils.Deref(creds.SecurityToken) != "" {
  112. apiRequest.query["SecurityToken"] = creds.SecurityToken
  113. }
  114. apiRequest.query["Signature"] = openapiutil.GetRPCSignature(apiRequest.query, esutils.Ptr(apiRequest.method.String()), creds.AccessKeySecret)
  115. httpReq, err := newHTTPRequestWithContext(ctx, apiRequest)
  116. if err != nil {
  117. return nil, fmt.Errorf("error creating http request: %w", err)
  118. }
  119. resp, err := s.client.Do(httpReq)
  120. if err != nil {
  121. return nil, fmt.Errorf("error invoking http request: %w", err)
  122. }
  123. defer func() {
  124. _ = resp.Body.Close()
  125. }()
  126. return s.parseResponse(resp)
  127. }
  128. func (s *secretsManagerClient) parseResponse(resp *http.Response) (map[string]any, error) {
  129. statusCode := esutils.Ptr(resp.StatusCode)
  130. if esutils.Deref(util.Is4xx(statusCode)) || esutils.Deref(util.Is5xx(statusCode)) {
  131. return nil, s.parseErrorResponse(resp)
  132. }
  133. obj, err := util.ReadAsJSON(resp.Body)
  134. if err != nil {
  135. return nil, err
  136. }
  137. res, err := util.AssertAsMap(obj)
  138. if err != nil {
  139. return nil, err
  140. }
  141. return res, nil
  142. }
  143. func (s *secretsManagerClient) parseErrorResponse(resp *http.Response) error {
  144. res, err := util.ReadAsJSON(resp.Body)
  145. if err != nil {
  146. return err
  147. }
  148. errorMap, err := util.AssertAsMap(res)
  149. if err != nil {
  150. return err
  151. }
  152. errorMap["statusCode"] = esutils.Ptr(resp.StatusCode)
  153. err = tea.NewSDKError(map[string]any{
  154. "code": tea.ToString(defaultAny(errorMap["Code"], errorMap["code"])),
  155. "message": fmt.Sprintf("code: %s, %s", tea.ToString(resp.StatusCode), tea.ToString(defaultAny(errorMap["Message"], errorMap["message"]))),
  156. "data": errorMap,
  157. "description": tea.ToString(defaultAny(errorMap["Description"], errorMap["description"])),
  158. "accessDeniedDetail": errorMap["AccessDeniedDetail"],
  159. })
  160. return err
  161. }
  162. type methodType string
  163. const (
  164. methodTypeGET = "GET"
  165. )
  166. func (m methodType) String() string {
  167. return string(m)
  168. }
  169. type openAPIRequest struct {
  170. endpoint string
  171. method methodType
  172. headers map[string]*string
  173. query map[string]*string
  174. }
  175. func newOpenAPIRequest(endpoint string,
  176. action string,
  177. method methodType,
  178. request any,
  179. ) *openAPIRequest {
  180. req := &openAPIRequest{
  181. endpoint: endpoint,
  182. method: method,
  183. headers: map[string]*string{
  184. "host": &endpoint,
  185. "x-acs-version": esutils.Ptr(kmsAPIVersion),
  186. "x-acs-action": &action,
  187. "user-agent": esutils.Ptr(fmt.Sprintf("AlibabaCloud (%s; %s) Golang/%s Core/%s TeaDSL/1", runtime.GOOS, runtime.GOARCH, strings.Trim(runtime.Version(), "go"), "0.01")),
  188. },
  189. query: map[string]*string{
  190. "Action": &action,
  191. "Format": esutils.Ptr("json"),
  192. "Version": esutils.Ptr(kmsAPIVersion),
  193. "Timestamp": openapiutil.GetTimestamp(),
  194. "SignatureNonce": util.GetNonce(),
  195. "SignatureMethod": esutils.Ptr("HMAC-SHA1"),
  196. "SignatureVersion": esutils.Ptr("1.0"),
  197. },
  198. }
  199. req.query = tea.Merge(req.query, openapiutil.Query(request))
  200. return req
  201. }
  202. func newHTTPRequestWithContext(ctx context.Context,
  203. req *openAPIRequest) (*http.Request, error) {
  204. query := url.Values{}
  205. for k, v := range req.query {
  206. query.Add(k, esutils.Deref(v))
  207. }
  208. httpReq, err := http.NewRequestWithContext(ctx, req.method.String(), fmt.Sprintf("https://%s/?%s", url.PathEscape(req.endpoint), query.Encode()), http.NoBody)
  209. if err != nil {
  210. return nil, fmt.Errorf("error converting OpenAPI request to http request: %w", err)
  211. }
  212. for k, v := range req.headers {
  213. httpReq.Header.Add(k, esutils.Deref(v))
  214. }
  215. return httpReq, nil
  216. }
  217. func defaultAny(inputValue, defaultValue any) any {
  218. if esutils.Deref(util.IsUnset(inputValue)) {
  219. return defaultValue
  220. }
  221. return inputValue
  222. }