provider.go 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  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 secretmanager
  13. import (
  14. "context"
  15. "crypto/tls"
  16. "errors"
  17. "fmt"
  18. "net/url"
  19. "os"
  20. "sync"
  21. "time"
  22. authV1 "github.com/cloudru-tech/iam-sdk/api/auth/v1"
  23. smssdk "github.com/cloudru-tech/secret-manager-sdk"
  24. "github.com/google/uuid"
  25. "google.golang.org/grpc"
  26. "google.golang.org/grpc/credentials"
  27. "google.golang.org/grpc/keepalive"
  28. kclient "sigs.k8s.io/controller-runtime/pkg/client"
  29. "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
  30. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  31. "github.com/external-secrets/external-secrets/pkg/provider/cloudru/secretmanager/adapter"
  32. "github.com/external-secrets/external-secrets/pkg/utils"
  33. )
  34. func init() {
  35. esv1.Register(NewProvider(), &esv1.SecretStoreProvider{CloudruSM: &esv1.CloudruSMProvider{}}, esv1.MaintenanceStatusMaintained)
  36. }
  37. var _ esv1.Provider = &Provider{}
  38. var _ esv1.SecretsClient = &Client{}
  39. // Provider is a secrets provider for Cloud.ru Secret Manager.
  40. type Provider struct {
  41. mu sync.Mutex
  42. // clients is a map of Cloud.ru Secret Manager clients.
  43. // Is used to cache the clients to avoid multiple connections,
  44. // and excess token retrieving with same credentials.
  45. clients map[string]*adapter.APIClient
  46. }
  47. // NewProvider creates a new Cloud.ru Secret Manager Provider.
  48. func NewProvider() *Provider {
  49. return &Provider{
  50. clients: make(map[string]*adapter.APIClient),
  51. }
  52. }
  53. // NewClient constructs a Cloud.ru Secret Manager Provider.
  54. func (p *Provider) NewClient(
  55. ctx context.Context,
  56. store esv1.GenericStore,
  57. kube kclient.Client,
  58. namespace string,
  59. ) (esv1.SecretsClient, error) {
  60. if _, err := p.ValidateStore(store); err != nil {
  61. return nil, fmt.Errorf("invalid store: %w", err)
  62. }
  63. csmRef := store.GetSpec().Provider.CloudruSM
  64. storeKind := store.GetObjectKind().GroupVersionKind().Kind
  65. cr := NewKubeCredentialsResolver(kube, namespace, storeKind, csmRef.Auth.SecretRef)
  66. client, err := p.getClient(ctx, cr)
  67. if err != nil {
  68. return nil, fmt.Errorf("failed to connect cloud.ru services: %w", err)
  69. }
  70. return &Client{
  71. apiClient: client,
  72. projectID: csmRef.ProjectID,
  73. }, nil
  74. }
  75. func (p *Provider) getClient(ctx context.Context, cr adapter.CredentialsResolver) (*adapter.APIClient, error) {
  76. p.mu.Lock()
  77. defer p.mu.Unlock()
  78. discoveryURL, tokenURL, smURL, err := provideEndpoints()
  79. if err != nil {
  80. return nil, fmt.Errorf("parse endpoint URLs: %w", err)
  81. }
  82. creds, err := cr.Resolve(ctx)
  83. if err != nil {
  84. return nil, fmt.Errorf("resolve API credentials: %w", err)
  85. }
  86. connStack := fmt.Sprintf("%s,%s+%s", discoveryURL, creds.KeyID, creds.Secret)
  87. client, ok := p.clients[connStack]
  88. if ok {
  89. return client, nil
  90. }
  91. iamConn, err := grpc.NewClient(tokenURL,
  92. grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{MinVersion: tls.VersionTLS13})),
  93. grpc.WithKeepaliveParams(keepalive.ClientParameters{
  94. Time: time.Second * 30,
  95. Timeout: time.Second * 5,
  96. PermitWithoutStream: false,
  97. }),
  98. grpc.WithUserAgent("external-secrets"),
  99. )
  100. if err != nil {
  101. return nil, fmt.Errorf("initialize cloud.ru IAM gRPC client: initiate connection: %w", err)
  102. }
  103. smsClient, err := smssdk.New(&smssdk.Config{Host: smURL},
  104. grpc.WithKeepaliveParams(keepalive.ClientParameters{
  105. Time: time.Second * 30,
  106. Timeout: time.Second * 5,
  107. PermitWithoutStream: false,
  108. }),
  109. grpc.WithUserAgent("external-secrets"),
  110. )
  111. if err != nil {
  112. return nil, fmt.Errorf("initialize cloud.ru Secret Manager gRPC client: initiate connection: %w", err)
  113. }
  114. iamClient := authV1.NewAuthServiceClient(iamConn)
  115. client = adapter.NewAPIClient(cr, iamClient, smsClient)
  116. p.clients[connStack] = client
  117. return client, nil
  118. }
  119. // ValidateStore validates the store specification.
  120. func (p *Provider) ValidateStore(store esv1.GenericStore) (admission.Warnings, error) {
  121. if store == nil {
  122. return nil, errors.New("store is not provided")
  123. }
  124. spec := store.GetSpec()
  125. if spec == nil || spec.Provider == nil || spec.Provider.CloudruSM == nil {
  126. return nil, errors.New("csm spec is not provided")
  127. }
  128. csmProvider := spec.Provider.CloudruSM
  129. switch {
  130. case csmProvider.Auth.SecretRef == nil:
  131. return nil, errors.New("invalid spec: auth.secretRef is required")
  132. case csmProvider.ProjectID == "":
  133. return nil, errors.New("invalid spec: projectID is required")
  134. }
  135. if _, err := uuid.Parse(csmProvider.ProjectID); err != nil {
  136. return nil, fmt.Errorf("invalid spec: projectID is invalid UUID: %w", err)
  137. }
  138. ref := csmProvider.Auth.SecretRef
  139. err := utils.ValidateReferentSecretSelector(store, ref.AccessKeyID)
  140. if err != nil {
  141. return nil, fmt.Errorf("invalid spec: auth.secretRef.accessKeyID: %w", err)
  142. }
  143. err = utils.ValidateReferentSecretSelector(store, ref.AccessKeySecret)
  144. if err != nil {
  145. return nil, fmt.Errorf("invalid spec: auth.secretRef.accessKeySecret: %w", err)
  146. }
  147. return nil, nil
  148. }
  149. // Capabilities returns the provider Capabilities (ReadOnly).
  150. func (p *Provider) Capabilities() esv1.SecretStoreCapabilities {
  151. return esv1.SecretStoreReadOnly
  152. }
  153. func provideEndpoints() (discoveryURL, tokenURL, smURL string, err error) {
  154. discoveryURL = EndpointsURI
  155. if du := os.Getenv("CLOUDRU_DISCOVERY_URL"); du != "" {
  156. var u *url.URL
  157. u, err = url.Parse(du)
  158. if err != nil {
  159. return "", "", "", fmt.Errorf("parse discovery URL: %w", err)
  160. }
  161. if u.Scheme != "https" && u.Scheme != "http" {
  162. return "", "", "", fmt.Errorf("invalid scheme in discovery URL, expected http or https, got %s", u.Scheme)
  163. }
  164. discoveryURL = du
  165. }
  166. // try to get the endpoints from the environment variables.
  167. csmAddress := os.Getenv("CLOUDRU_CSM_ADDRESS")
  168. iamAddress := os.Getenv("CLOUDRU_IAM_ADDRESS")
  169. if csmAddress != "" && iamAddress != "" {
  170. return discoveryURL, iamAddress, csmAddress, nil
  171. }
  172. // using the discovery URL to get the endpoints.
  173. var endpoints *EndpointsResponse
  174. endpoints, err = GetEndpoints(discoveryURL)
  175. if err != nil {
  176. return "", "", "", fmt.Errorf("discover cloud.ru API endpoints: %w", err)
  177. }
  178. smEndpoint := endpoints.Get("secret-manager")
  179. if smEndpoint == nil {
  180. return "", "", "", errors.New("secret-manager API is not available")
  181. }
  182. iamEndpoint := endpoints.Get("iam")
  183. if iamEndpoint == nil {
  184. return "", "", "", errors.New("iam API is not available")
  185. }
  186. return discoveryURL, iamEndpoint.Address, smEndpoint.Address, nil
  187. }