resilient.go 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  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(
  77. ctx context.Context,
  78. secret *corev1.Secret,
  79. pushSecretData *pb.PushSecretData,
  80. providerRef *pb.ProviderReference,
  81. compatibilityStore *pb.CompatibilityStore,
  82. sourceNamespace string,
  83. ) error {
  84. return rc.executeWithResilience(ctx, func(client v2.Provider) error {
  85. return client.PushSecret(ctx, secret, pushSecretData, providerRef, compatibilityStore, sourceNamespace)
  86. })
  87. }
  88. // DeleteSecret deletes a secret with retry logic and circuit breaking.
  89. func (rc *ResilientClient) DeleteSecret(
  90. ctx context.Context,
  91. remoteRef *pb.PushSecretRemoteRef,
  92. providerRef *pb.ProviderReference,
  93. compatibilityStore *pb.CompatibilityStore,
  94. sourceNamespace string,
  95. ) error {
  96. return rc.executeWithResilience(ctx, func(client v2.Provider) error {
  97. return client.DeleteSecret(ctx, remoteRef, providerRef, compatibilityStore, sourceNamespace)
  98. })
  99. }
  100. // SecretExists checks if a secret exists with retry logic and circuit breaking.
  101. func (rc *ResilientClient) SecretExists(
  102. ctx context.Context,
  103. remoteRef *pb.PushSecretRemoteRef,
  104. providerRef *pb.ProviderReference,
  105. compatibilityStore *pb.CompatibilityStore,
  106. sourceNamespace string,
  107. ) (bool, error) {
  108. var result bool
  109. err := rc.executeWithResilience(ctx, func(client v2.Provider) error {
  110. exists, err := client.SecretExists(ctx, remoteRef, providerRef, compatibilityStore, sourceNamespace)
  111. if err != nil {
  112. return err
  113. }
  114. result = exists
  115. return nil
  116. })
  117. return result, err
  118. }
  119. // GetSecret retrieves a secret with retry logic and circuit breaking.
  120. func (rc *ResilientClient) GetSecret(
  121. ctx context.Context,
  122. ref esv1.ExternalSecretDataRemoteRef,
  123. providerRef *pb.ProviderReference,
  124. compatibilityStore *pb.CompatibilityStore,
  125. sourceNamespace string,
  126. ) ([]byte, error) {
  127. var result []byte
  128. err := rc.executeWithResilience(ctx, func(client v2.Provider) error {
  129. secretData, err := client.GetSecret(ctx, ref, providerRef, compatibilityStore, sourceNamespace)
  130. if err != nil {
  131. return err
  132. }
  133. result = secretData
  134. return nil
  135. })
  136. return result, err
  137. }
  138. // GetSecretMap retrieves multiple key/value pairs from a single provider object with retry logic and circuit breaking.
  139. func (rc *ResilientClient) GetSecretMap(
  140. ctx context.Context,
  141. ref esv1.ExternalSecretDataRemoteRef,
  142. providerRef *pb.ProviderReference,
  143. compatibilityStore *pb.CompatibilityStore,
  144. sourceNamespace string,
  145. ) (map[string][]byte, error) {
  146. var result map[string][]byte
  147. err := rc.executeWithResilience(ctx, func(client v2.Provider) error {
  148. secretMap, err := client.GetSecretMap(ctx, ref, providerRef, compatibilityStore, sourceNamespace)
  149. if err != nil {
  150. return err
  151. }
  152. result = secretMap
  153. return nil
  154. })
  155. return result, err
  156. }
  157. // GetAllSecrets retrieves multiple secrets with retry logic and circuit breaking.
  158. func (rc *ResilientClient) GetAllSecrets(
  159. ctx context.Context,
  160. find esv1.ExternalSecretFind,
  161. providerRef *pb.ProviderReference,
  162. compatibilityStore *pb.CompatibilityStore,
  163. sourceNamespace string,
  164. ) (map[string][]byte, error) {
  165. var result map[string][]byte
  166. err := rc.executeWithResilience(ctx, func(client v2.Provider) error {
  167. secrets, err := client.GetAllSecrets(ctx, find, providerRef, compatibilityStore, sourceNamespace)
  168. if err != nil {
  169. return err
  170. }
  171. result = secrets
  172. return nil
  173. })
  174. return result, err
  175. }
  176. // Validate validates the provider configuration with retry logic.
  177. func (rc *ResilientClient) Validate(ctx context.Context, providerRef *pb.ProviderReference, compatibilityStore *pb.CompatibilityStore, sourceNamespace string) error {
  178. return rc.executeWithResilience(ctx, func(client v2.Provider) error {
  179. return client.Validate(ctx, providerRef, compatibilityStore, sourceNamespace)
  180. })
  181. }
  182. // Capabilities retrieves the provider's capabilities with retry logic.
  183. func (rc *ResilientClient) Capabilities(ctx context.Context, providerRef *pb.ProviderReference, sourceNamespace string) (pb.SecretStoreCapabilities, error) {
  184. var result pb.SecretStoreCapabilities
  185. err := rc.executeWithResilience(ctx, func(client v2.Provider) error {
  186. caps, err := client.Capabilities(ctx, providerRef, sourceNamespace)
  187. if err != nil {
  188. return err
  189. }
  190. result = caps
  191. return nil
  192. })
  193. return result, err
  194. }
  195. // Close closes the connection pool.
  196. func (rc *ResilientClient) Close(_ context.Context) error {
  197. return rc.pool.Close()
  198. }
  199. // executeWithResilience executes a function with connection pooling, retry, and circuit breaking.
  200. func (rc *ResilientClient) executeWithResilience(ctx context.Context, fn func(v2.Provider) error) error {
  201. rc.log.V(1).Info("executing with resilience",
  202. "circuitState", rc.circuitBreaker.State())
  203. // Check circuit breaker first
  204. return rc.circuitBreaker.Call(ctx, func() error {
  205. // Execute with retry
  206. return WithRetry(ctx, rc.retryConfig, func(ctx context.Context, attempt int) error {
  207. if attempt > 1 {
  208. rc.log.Info("retrying operation",
  209. "attempt", attempt,
  210. "maxAttempts", rc.retryConfig.MaxAttempts)
  211. }
  212. // Get connection from pool
  213. rc.log.V(1).Info("getting connection from pool")
  214. client, err := rc.pool.Get(ctx, rc.address, rc.tlsConfig)
  215. if err != nil {
  216. rc.log.Error(err, "failed to get connection from pool",
  217. "attempt", attempt)
  218. return fmt.Errorf("failed to get connection from pool: %w", err)
  219. }
  220. defer rc.pool.Release(rc.address, rc.tlsConfig)
  221. // Execute the function
  222. rc.log.V(1).Info("executing provider operation", "attempt", attempt)
  223. err = fn(client)
  224. if err != nil {
  225. rc.log.Error(err, "provider operation failed",
  226. "attempt", attempt,
  227. "willRetry", attempt < rc.retryConfig.MaxAttempts)
  228. }
  229. return err
  230. })
  231. })
  232. }
  233. // GetCircuitState returns the current state of the circuit breaker.
  234. func (rc *ResilientClient) GetCircuitState() CircuitState {
  235. return rc.circuitBreaker.State()
  236. }