provider.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  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 mysterybox contains the logic to work with Nebius MysteryBox API.
  14. package mysterybox
  15. import (
  16. "context"
  17. "errors"
  18. "fmt"
  19. "strings"
  20. "sync"
  21. "github.com/go-logr/logr"
  22. lru "github.com/hashicorp/golang-lru"
  23. "github.com/spf13/pflag"
  24. "k8s.io/utils/clock"
  25. ctrl "sigs.k8s.io/controller-runtime"
  26. "sigs.k8s.io/controller-runtime/pkg/client"
  27. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  28. esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
  29. "github.com/external-secrets/external-secrets/providers/v1/nebius/common/sdk/iam"
  30. "github.com/external-secrets/external-secrets/providers/v1/nebius/common/sdk/mysterybox"
  31. "github.com/external-secrets/external-secrets/runtime/constants"
  32. "github.com/external-secrets/external-secrets/runtime/esutils/resolvers"
  33. "github.com/external-secrets/external-secrets/runtime/feature"
  34. "github.com/external-secrets/external-secrets/runtime/metrics"
  35. )
  36. var (
  37. log = ctrl.Log.WithName("provider").WithName("nebius").WithName("mysterybox")
  38. mysteryboxTokensCacheSize int
  39. mysteryboxConnectionsCacheSize int
  40. defaultTokenCacheSize = 2 << 11
  41. defaultMysteryboxConnectionsCacheSize = 2 << 6
  42. )
  43. // NewMysteryboxClient is a function that describes how to create a Nebius MysteryBox client to interact within.
  44. type NewMysteryboxClient func(ctx context.Context, apiDomain string, caCertificate []byte) (mysterybox.Client, error)
  45. // SecretsClientConfig holds configuration for interacting with.
  46. type SecretsClientConfig struct {
  47. APIDomain string
  48. ServiceAccountCreds *esmeta.SecretKeySelector
  49. Token *esmeta.SecretKeySelector
  50. CACertificate *esmeta.SecretKeySelector
  51. }
  52. // ClientCacheKey represents a unique key for identifying cached MysteryBox clients.
  53. // It is composed of an API domain and a hash of the CA certificate.
  54. type ClientCacheKey struct {
  55. APIDomain string
  56. CAHash string
  57. }
  58. // Provider is a struct for managing MysteryBox clients.
  59. type Provider struct {
  60. Logger logr.Logger
  61. NewMysteryboxClient NewMysteryboxClient
  62. TokenGetter TokenGetter
  63. mysteryboxClientsCache *lru.Cache
  64. tokenInitMutex sync.Mutex
  65. cacheInitMutex sync.Mutex
  66. mysteryboxClientsCacheMutex sync.Mutex
  67. }
  68. // Capabilities returns the capabilities of the secret store, indicating it is read-only.
  69. func (p *Provider) Capabilities() esv1.SecretStoreCapabilities {
  70. return esv1.SecretStoreReadOnly
  71. }
  72. // NewClient creates and returns a new SecretsClient for the specified SecretStore and namespace context.
  73. func (p *Provider) NewClient(ctx context.Context, store esv1.GenericStore, kube client.Client, namespace string) (esv1.SecretsClient, error) {
  74. clientConfig, err := parseConfig(store)
  75. if err != nil {
  76. return nil, err
  77. }
  78. var caCert []byte
  79. if clientConfig.CACertificate != nil {
  80. caCert, err = p.getCaCert(ctx, clientConfig, store, kube, namespace)
  81. if err != nil {
  82. return nil, fmt.Errorf("read CA certificate %s/%s: %w", namespace, clientConfig.CACertificate.Name, err)
  83. }
  84. }
  85. // lazy initialization with a current flag value
  86. if err = p.initTokenGetter(); err != nil {
  87. return nil, fmt.Errorf("init token getter: %w", err)
  88. }
  89. iamToken, err := p.getIamToken(ctx, clientConfig, store, kube, namespace, caCert)
  90. if err != nil {
  91. p.Logger.Info("Could not get IAM token", "store", store.GetNamespacedName(), "err", err)
  92. return nil, err
  93. }
  94. mysteryboxGrpcClient, err := p.createOrGetMysteryboxClient(ctx, clientConfig.APIDomain, caCert)
  95. if err != nil {
  96. p.Logger.Info("Could not create or get MysteryBox Client", "store", store.GetNamespacedName(), "err", err)
  97. return nil, err
  98. }
  99. return &SecretsClient{
  100. mysteryboxClient: mysteryboxGrpcClient,
  101. token: iamToken,
  102. }, nil
  103. }
  104. // getIamToken retrieves an IAM token based on the provided SecretsClientConfig and authentication options.
  105. // It supports token retrieval from a predefined secret or via service account credentials with the TokenGetter.
  106. func (p *Provider) getIamToken(ctx context.Context, config *SecretsClientConfig, store esv1.GenericStore, kube client.Client, namespace string, caCert []byte) (string, error) {
  107. if config.Token.Name != "" {
  108. iamToken, err := resolvers.SecretKeyRef(
  109. ctx,
  110. kube,
  111. store.GetKind(),
  112. namespace,
  113. config.Token,
  114. )
  115. if err != nil {
  116. return "", fmt.Errorf("read token secret %s/%s: %w", namespace, config.Token.Name, err)
  117. }
  118. return strings.TrimSpace(iamToken), nil
  119. }
  120. if config.ServiceAccountCreds.Name != "" {
  121. subjectCreds, err := resolvers.SecretKeyRef(
  122. ctx,
  123. kube,
  124. store.GetKind(),
  125. namespace,
  126. config.ServiceAccountCreds,
  127. )
  128. if err != nil {
  129. return "", fmt.Errorf("read service account creds %s/%s: %w", namespace, config.ServiceAccountCreds.Name, err)
  130. }
  131. token, err := p.TokenGetter.GetToken(ctx, config.APIDomain, subjectCreds, caCert)
  132. if err != nil {
  133. return "", fmt.Errorf(errFailedToRetrieveToken, err)
  134. }
  135. return strings.TrimSpace(token), nil
  136. }
  137. return "", errors.New(errMissingAuthOptions)
  138. }
  139. // createOrGetMysteryboxClient initializes or retrieves a cached MysteryBox client for a specified API domain and certificate.
  140. func (p *Provider) createOrGetMysteryboxClient(ctx context.Context, apiDomain string, caCertificate []byte) (mysterybox.Client, error) {
  141. // lazy initialization with a current flag value
  142. if err := p.initMysteryboxClientsCache(); err != nil {
  143. return nil, err
  144. }
  145. cacheKey := ClientCacheKey{
  146. APIDomain: apiDomain,
  147. CAHash: HashBytes(caCertificate),
  148. }
  149. // lock to avoid race and connections leaks during client creation for the same key
  150. p.mysteryboxClientsCacheMutex.Lock()
  151. defer p.mysteryboxClientsCacheMutex.Unlock()
  152. if value, ok := p.mysteryboxClientsCache.Get(cacheKey); ok {
  153. p.Logger.V(1).Info("Reusing cached MysteryBox client", "apiDomain", apiDomain)
  154. return value.(mysterybox.Client), nil
  155. }
  156. p.Logger.Info("Creating a new MysteryBox client", "apiDomain", apiDomain)
  157. mysteryboxClient, err := p.NewMysteryboxClient(ctx, apiDomain, caCertificate)
  158. if err != nil {
  159. return nil, err
  160. }
  161. p.mysteryboxClientsCache.Add(cacheKey, mysteryboxClient)
  162. return mysteryboxClient, nil
  163. }
  164. // getCaCert retrieves and returns the CA certificate as a byte slice for the specified secret in the given namespace.
  165. func (p *Provider) getCaCert(ctx context.Context, config *SecretsClientConfig, store esv1.GenericStore, kube client.Client, namespace string) ([]byte, error) {
  166. caCert, err := resolvers.SecretKeyRef(
  167. ctx,
  168. kube,
  169. store.GetKind(),
  170. namespace,
  171. config.CACertificate,
  172. )
  173. if err != nil {
  174. return nil, err
  175. }
  176. return []byte(strings.TrimSpace(caCert)), nil
  177. }
  178. func parseConfig(store esv1.GenericStore) (*SecretsClientConfig, error) {
  179. nebiusMysteryboxProvider, err := getNebiusMysteryboxProvider(store)
  180. if err != nil {
  181. return nil, err
  182. }
  183. if nebiusMysteryboxProvider.APIDomain == "" {
  184. return nil, errors.New(errMissingAPIDomain)
  185. }
  186. var caCertificate *esmeta.SecretKeySelector
  187. if nebiusMysteryboxProvider.CAProvider != nil {
  188. caCertificate = &nebiusMysteryboxProvider.CAProvider.Certificate
  189. }
  190. return &SecretsClientConfig{
  191. APIDomain: strings.TrimSpace(nebiusMysteryboxProvider.APIDomain),
  192. ServiceAccountCreds: &nebiusMysteryboxProvider.Auth.ServiceAccountCreds,
  193. Token: &nebiusMysteryboxProvider.Auth.Token,
  194. CACertificate: caCertificate,
  195. }, nil
  196. }
  197. func newMysteryboxClient(ctx context.Context, apiDomain string, caCertificate []byte) (mysterybox.Client, error) {
  198. return mysterybox.NewNebiusMysteryboxClientGrpc(ctx, apiDomain, caCertificate)
  199. }
  200. func (p *Provider) initMysteryboxClientsCache() error {
  201. p.cacheInitMutex.Lock()
  202. defer p.cacheInitMutex.Unlock()
  203. if p.mysteryboxClientsCache != nil {
  204. return nil
  205. }
  206. var err error
  207. var cache *lru.Cache
  208. cache, err = lru.NewWithEvict(
  209. mysteryboxConnectionsCacheSize,
  210. func(key, _ interface{}) {
  211. p.Logger.V(1).Info("Evicting a Nebius MysteryBox client", "apiDomain", key.(ClientCacheKey).APIDomain)
  212. // We intentionally do not call Close() on the evicted client here.
  213. // This avoids "dial is closed" errors for active
  214. // reconciliation loops that might still be using this client instance
  215. // at the moment of eviction.
  216. //
  217. // If this approach leads to resource leaks in the future, we should consider
  218. // implementing a reference counter to safely close the client only when
  219. // it's no longer used by any active session.
  220. },
  221. )
  222. if err == nil {
  223. p.mysteryboxClientsCache = cache
  224. return nil
  225. }
  226. return fmt.Errorf("init clients cache: %w", err)
  227. }
  228. func (p *Provider) initTokenGetter() error {
  229. p.tokenInitMutex.Lock()
  230. defer p.tokenInitMutex.Unlock()
  231. if p.TokenGetter != nil {
  232. return nil
  233. }
  234. var err error
  235. c := clock.RealClock{}
  236. tokenExchangerLogger := ctrl.Log.WithName("provider").WithName("nebius").WithName("iam").WithName("grpctokenexchanger")
  237. tokenExchangeObserveFunction := func(err error) {
  238. metrics.ObserveAPICall(constants.ProviderNebiusMysterybox, constants.CallNebiusMysteryboxAuth, err)
  239. }
  240. var tokenGetter TokenGetter
  241. tokenGetter, err = NewCachedTokenGetter(
  242. mysteryboxTokensCacheSize,
  243. iam.NewGrpcTokenExchanger(
  244. tokenExchangerLogger,
  245. tokenExchangeObserveFunction,
  246. ), c)
  247. if err == nil {
  248. p.TokenGetter = tokenGetter
  249. }
  250. return err
  251. }
  252. // NewProvider creates a new Provider instance.
  253. func NewProvider() esv1.Provider {
  254. return &Provider{
  255. Logger: log,
  256. NewMysteryboxClient: newMysteryboxClient,
  257. }
  258. }
  259. // MaintenanceStatus returns the maintenance status of the provider.
  260. func MaintenanceStatus() esv1.MaintenanceStatus {
  261. return esv1.MaintenanceStatusMaintained
  262. }
  263. // ProviderSpec returns the provider specification for registration.
  264. func ProviderSpec() *esv1.SecretStoreProvider {
  265. return &esv1.SecretStoreProvider{
  266. NebiusMysterybox: &esv1.NebiusMysteryboxProvider{},
  267. }
  268. }
  269. func init() {
  270. fs := pflag.NewFlagSet("nebius", pflag.ExitOnError)
  271. fs.IntVar(
  272. &mysteryboxTokensCacheSize,
  273. "mysterybox-tokens-cache-size",
  274. defaultTokenCacheSize,
  275. "Size of Nebius MysteryBox token cache. "+
  276. "External secrets will reuse the Nebius IAM token without requesting a new one on each request.",
  277. )
  278. fs.IntVar(
  279. &mysteryboxConnectionsCacheSize,
  280. "mysterybox-connections-cache-size",
  281. defaultMysteryboxConnectionsCacheSize,
  282. "Size of Nebius MysteryBox grpc clients cache. External secrets will reuse the "+
  283. "connection to MysteryBox for the configuration without opening a new one on each request.",
  284. )
  285. feature.Register(feature.Feature{
  286. Flags: fs,
  287. Initialize: func() {
  288. if mysteryboxTokensCacheSize <= 0 {
  289. log.Error(nil, "invalid token cache size, use default",
  290. "got", mysteryboxTokensCacheSize,
  291. "default", defaultTokenCacheSize,
  292. )
  293. mysteryboxTokensCacheSize = defaultTokenCacheSize
  294. }
  295. if mysteryboxConnectionsCacheSize <= 0 {
  296. log.Error(nil, "invalid connections cache size, use default",
  297. "got", mysteryboxConnectionsCacheSize,
  298. "default", defaultMysteryboxConnectionsCacheSize,
  299. )
  300. mysteryboxConnectionsCacheSize = defaultMysteryboxConnectionsCacheSize
  301. }
  302. log.Info(
  303. "Registered Nebius MysteryBox provider",
  304. "token cache size", mysteryboxTokensCacheSize,
  305. "clients cache size", mysteryboxConnectionsCacheSize,
  306. )
  307. },
  308. })
  309. }