provider.go 6.7 KB

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