resilient.go 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  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 grpc
  14. import (
  15. "context"
  16. "fmt"
  17. "github.com/go-logr/logr"
  18. corev1 "k8s.io/api/core/v1"
  19. ctrl "sigs.k8s.io/controller-runtime"
  20. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  21. pb "github.com/external-secrets/external-secrets/proto/provider"
  22. v2 "github.com/external-secrets/external-secrets/providers/v2/common"
  23. )
  24. // ResilientClient wraps a gRPC provider client with connection pooling,
  25. // retry logic, and circuit breaking for production-ready reliability.
  26. type ResilientClient struct {
  27. address string
  28. tlsConfig *TLSConfig
  29. pool *ConnectionPool
  30. circuitBreaker *CircuitBreaker
  31. retryConfig RetryConfig
  32. log logr.Logger
  33. }
  34. // ResilientClientConfig configures the resilient client.
  35. type ResilientClientConfig struct {
  36. Address string
  37. TLSConfig *TLSConfig
  38. PoolConfig PoolConfig
  39. CircuitConfig CircuitBreakerConfig
  40. RetryConfig RetryConfig
  41. }
  42. // DefaultResilientClientConfig returns sensible defaults.
  43. func DefaultResilientClientConfig(address string, tlsConfig *TLSConfig) ResilientClientConfig {
  44. return ResilientClientConfig{
  45. Address: address,
  46. TLSConfig: tlsConfig,
  47. PoolConfig: DefaultPoolConfig(),
  48. CircuitConfig: DefaultCircuitBreakerConfig(),
  49. RetryConfig: DefaultRetryConfig(),
  50. }
  51. }
  52. // NewResilientClient creates a new resilient client with connection pooling,
  53. // retry logic, and circuit breaking.
  54. func NewResilientClient(config ResilientClientConfig) (*ResilientClient, error) {
  55. if config.Address == "" {
  56. return nil, fmt.Errorf("provider address cannot be empty")
  57. }
  58. log := ctrl.Log.WithName("grpc-resilient").WithValues("address", config.Address)
  59. log.Info("creating resilient client",
  60. "poolMaxIdleTime", config.PoolConfig.MaxIdleTime.String(),
  61. "poolMaxLifetime", config.PoolConfig.MaxLifetime.String(),
  62. "retryMaxAttempts", config.RetryConfig.MaxAttempts,
  63. "circuitMaxFailures", config.CircuitConfig.MaxFailures)
  64. return &ResilientClient{
  65. address: config.Address,
  66. tlsConfig: config.TLSConfig,
  67. pool: NewConnectionPool(config.PoolConfig),
  68. circuitBreaker: NewCircuitBreaker(config.CircuitConfig),
  69. retryConfig: config.RetryConfig,
  70. log: log,
  71. }, nil
  72. }
  73. // Ensure ResilientClient implements the Provider interface.
  74. var _ v2.Provider = &ResilientClient{}
  75. // PushSecret writes a secret with retry logic and circuit breaking.
  76. func (rc *ResilientClient) PushSecret(ctx context.Context, secret *corev1.Secret, pushSecretData *pb.PushSecretData, providerRef *pb.ProviderReference, sourceNamespace string) error {
  77. return rc.executeWithResilience(ctx, func(client v2.Provider) error {
  78. return client.PushSecret(ctx, secret, pushSecretData, providerRef, sourceNamespace)
  79. })
  80. }
  81. // DeleteSecret deletes a secret with retry logic and circuit breaking.
  82. func (rc *ResilientClient) DeleteSecret(ctx context.Context, remoteRef *pb.PushSecretRemoteRef, providerRef *pb.ProviderReference, sourceNamespace string) error {
  83. return rc.executeWithResilience(ctx, func(client v2.Provider) error {
  84. return client.DeleteSecret(ctx, remoteRef, providerRef, sourceNamespace)
  85. })
  86. }
  87. // SecretExists checks if a secret exists with retry logic and circuit breaking.
  88. func (rc *ResilientClient) SecretExists(ctx context.Context, remoteRef *pb.PushSecretRemoteRef, providerRef *pb.ProviderReference, sourceNamespace string) (bool, error) {
  89. var result bool
  90. err := rc.executeWithResilience(ctx, func(client v2.Provider) error {
  91. exists, err := client.SecretExists(ctx, remoteRef, providerRef, sourceNamespace)
  92. if err != nil {
  93. return err
  94. }
  95. result = exists
  96. return nil
  97. })
  98. return result, err
  99. }
  100. // GetSecret retrieves a secret with retry logic and circuit breaking.
  101. func (rc *ResilientClient) GetSecret(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef, providerRef *pb.ProviderReference, sourceNamespace string) ([]byte, error) {
  102. var result []byte
  103. err := rc.executeWithResilience(ctx, func(client v2.Provider) error {
  104. secretData, err := client.GetSecret(ctx, ref, providerRef, sourceNamespace)
  105. if err != nil {
  106. return err
  107. }
  108. result = secretData
  109. return nil
  110. })
  111. return result, err
  112. }
  113. // GetSecretMap retrieves multiple key/value pairs from a single provider object with retry logic and circuit breaking.
  114. func (rc *ResilientClient) GetSecretMap(ctx context.Context, ref esv1.ExternalSecretDataRemoteRef, providerRef *pb.ProviderReference, sourceNamespace string) (map[string][]byte, error) {
  115. var result map[string][]byte
  116. err := rc.executeWithResilience(ctx, func(client v2.Provider) error {
  117. secretMap, err := client.GetSecretMap(ctx, ref, providerRef, sourceNamespace)
  118. if err != nil {
  119. return err
  120. }
  121. result = secretMap
  122. return nil
  123. })
  124. return result, err
  125. }
  126. // GetAllSecrets retrieves multiple secrets with retry logic and circuit breaking.
  127. func (rc *ResilientClient) GetAllSecrets(ctx context.Context, find esv1.ExternalSecretFind, providerRef *pb.ProviderReference, sourceNamespace string) (map[string][]byte, error) {
  128. var result map[string][]byte
  129. err := rc.executeWithResilience(ctx, func(client v2.Provider) error {
  130. secrets, err := client.GetAllSecrets(ctx, find, providerRef, sourceNamespace)
  131. if err != nil {
  132. return err
  133. }
  134. result = secrets
  135. return nil
  136. })
  137. return result, err
  138. }
  139. // Validate validates the provider configuration with retry logic.
  140. func (rc *ResilientClient) Validate(ctx context.Context, providerRef *pb.ProviderReference, sourceNamespace string) error {
  141. return rc.executeWithResilience(ctx, func(client v2.Provider) error {
  142. return client.Validate(ctx, providerRef, sourceNamespace)
  143. })
  144. }
  145. // Capabilities retrieves the provider's capabilities with retry logic.
  146. func (rc *ResilientClient) Capabilities(ctx context.Context, providerRef *pb.ProviderReference, sourceNamespace string) (pb.SecretStoreCapabilities, error) {
  147. var result pb.SecretStoreCapabilities
  148. err := rc.executeWithResilience(ctx, func(client v2.Provider) error {
  149. caps, err := client.Capabilities(ctx, providerRef, sourceNamespace)
  150. if err != nil {
  151. return err
  152. }
  153. result = caps
  154. return nil
  155. })
  156. return result, err
  157. }
  158. // Close closes the connection pool.
  159. func (rc *ResilientClient) Close(_ context.Context) error {
  160. return rc.pool.Close()
  161. }
  162. // executeWithResilience executes a function with connection pooling, retry, and circuit breaking.
  163. func (rc *ResilientClient) executeWithResilience(ctx context.Context, fn func(v2.Provider) error) error {
  164. rc.log.V(1).Info("executing with resilience",
  165. "circuitState", rc.circuitBreaker.State())
  166. // Check circuit breaker first
  167. return rc.circuitBreaker.Call(ctx, func() error {
  168. // Execute with retry
  169. return WithRetry(ctx, rc.retryConfig, func(ctx context.Context, attempt int) error {
  170. if attempt > 1 {
  171. rc.log.Info("retrying operation",
  172. "attempt", attempt,
  173. "maxAttempts", rc.retryConfig.MaxAttempts)
  174. }
  175. // Get connection from pool
  176. rc.log.V(1).Info("getting connection from pool")
  177. client, err := rc.pool.Get(ctx, rc.address, rc.tlsConfig)
  178. if err != nil {
  179. rc.log.Error(err, "failed to get connection from pool",
  180. "attempt", attempt)
  181. return fmt.Errorf("failed to get connection from pool: %w", err)
  182. }
  183. defer rc.pool.Release(rc.address, rc.tlsConfig)
  184. // Execute the function
  185. rc.log.V(1).Info("executing provider operation", "attempt", attempt)
  186. err = fn(client)
  187. if err != nil {
  188. rc.log.Error(err, "provider operation failed",
  189. "attempt", attempt,
  190. "willRetry", attempt < rc.retryConfig.MaxAttempts)
  191. }
  192. return err
  193. })
  194. })
  195. }
  196. // GetCircuitState returns the current state of the circuit breaker.
  197. func (rc *ResilientClient) GetCircuitState() CircuitState {
  198. return rc.circuitBreaker.State()
  199. }