provider.go 6.9 KB

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