client_manager_test.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  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 secretstore
  14. import (
  15. "context"
  16. "testing"
  17. "github.com/go-logr/logr"
  18. "github.com/stretchr/testify/assert"
  19. "github.com/stretchr/testify/require"
  20. corev1 "k8s.io/api/core/v1"
  21. apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
  22. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  23. "k8s.io/apimachinery/pkg/runtime"
  24. utilruntime "k8s.io/apimachinery/pkg/util/runtime"
  25. clientgoscheme "k8s.io/client-go/kubernetes/scheme"
  26. "sigs.k8s.io/controller-runtime/pkg/client"
  27. fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
  28. "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
  29. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  30. )
  31. func TestManagerGet(t *testing.T) {
  32. scheme := runtime.NewScheme()
  33. // add kubernetes schemes
  34. utilruntime.Must(clientgoscheme.AddToScheme(scheme))
  35. utilruntime.Must(apiextensionsv1.AddToScheme(scheme))
  36. // add external-secrets schemes
  37. utilruntime.Must(esv1.AddToScheme(scheme))
  38. // We have a test provider to control
  39. // the behavior of the NewClient func.
  40. fakeProvider := &WrapProvider{}
  41. esv1.ForceRegister(fakeProvider, &esv1.SecretStoreProvider{
  42. AWS: &esv1.AWSProvider{},
  43. }, esv1.MaintenanceStatusMaintained)
  44. // fake clients are re-used to compare the
  45. // in-memory reference
  46. clientA := &MockFakeClient{id: "1"}
  47. clientB := &MockFakeClient{id: "2"}
  48. const testNamespace = "foo"
  49. readyStatus := esv1.SecretStoreStatus{
  50. Conditions: []esv1.SecretStoreStatusCondition{
  51. {
  52. Type: esv1.SecretStoreReady,
  53. Status: corev1.ConditionTrue,
  54. },
  55. },
  56. }
  57. fakeSpec := esv1.SecretStoreSpec{
  58. Provider: &esv1.SecretStoreProvider{
  59. AWS: &esv1.AWSProvider{},
  60. },
  61. }
  62. defaultStore := &esv1.SecretStore{
  63. TypeMeta: metav1.TypeMeta{Kind: esv1.SecretStoreKind},
  64. ObjectMeta: metav1.ObjectMeta{
  65. Name: "foo",
  66. Namespace: testNamespace,
  67. },
  68. Spec: fakeSpec,
  69. Status: readyStatus,
  70. }
  71. otherStore := &esv1.SecretStore{
  72. TypeMeta: metav1.TypeMeta{Kind: esv1.SecretStoreKind},
  73. ObjectMeta: metav1.ObjectMeta{
  74. Name: "other",
  75. Namespace: testNamespace,
  76. },
  77. Spec: fakeSpec,
  78. Status: readyStatus,
  79. }
  80. var mgr *Manager
  81. provKey := clientKey{
  82. providerType: "*secretstore.WrapProvider",
  83. }
  84. type fields struct {
  85. client client.Client
  86. clientMap map[clientKey]*clientVal
  87. }
  88. type args struct {
  89. storeRef esv1.SecretStoreRef
  90. namespace string
  91. sourceRef *esv1.StoreGeneratorSourceRef
  92. }
  93. tests := []struct {
  94. name string
  95. fields fields
  96. args args
  97. clientConstructor func(
  98. ctx context.Context,
  99. store esv1.GenericStore,
  100. kube client.Client,
  101. namespace string) (esv1.SecretsClient, error)
  102. verify func(esv1.SecretsClient)
  103. afterClose func()
  104. want esv1.SecretsClient
  105. wantErr bool
  106. }{
  107. {
  108. name: "creates a new client from storeRef and stores it",
  109. wantErr: false,
  110. fields: fields{
  111. client: fakeclient.NewClientBuilder().
  112. WithScheme(scheme).
  113. WithObjects(defaultStore).
  114. Build(),
  115. clientMap: make(map[clientKey]*clientVal),
  116. },
  117. args: args{
  118. storeRef: esv1.SecretStoreRef{
  119. Name: defaultStore.Name,
  120. Kind: esv1.SecretStoreKind,
  121. },
  122. namespace: defaultStore.Namespace,
  123. sourceRef: nil,
  124. },
  125. clientConstructor: func(_ context.Context, _ esv1.GenericStore, _ client.Client, _ string) (esv1.SecretsClient, error) {
  126. return clientA, nil
  127. },
  128. verify: func(sc esv1.SecretsClient) {
  129. // we now must have this provider in the clientMap
  130. // and it mustbe the client defined in clientConstructor
  131. assert.NotNil(t, sc)
  132. c, ok := mgr.clientMap[provKey]
  133. require.True(t, ok)
  134. assert.Same(t, c.client, clientA)
  135. },
  136. afterClose: func() {
  137. v, ok := mgr.clientMap[provKey]
  138. assert.False(t, ok)
  139. assert.Nil(t, v)
  140. },
  141. },
  142. {
  143. name: "creates a new client using both storeRef and sourceRef",
  144. wantErr: false,
  145. fields: fields{
  146. client: fakeclient.NewClientBuilder().
  147. WithScheme(scheme).
  148. WithObjects(otherStore).
  149. Build(),
  150. clientMap: make(map[clientKey]*clientVal),
  151. },
  152. args: args{
  153. storeRef: esv1.SecretStoreRef{
  154. Name: defaultStore.Name,
  155. Kind: esv1.SecretStoreKind,
  156. },
  157. // this should take precedence
  158. sourceRef: &esv1.StoreGeneratorSourceRef{
  159. SecretStoreRef: &esv1.SecretStoreRef{
  160. Name: otherStore.Name,
  161. Kind: esv1.SecretStoreKind,
  162. },
  163. },
  164. namespace: defaultStore.Namespace,
  165. },
  166. clientConstructor: func(_ context.Context, _ esv1.GenericStore, _ client.Client, _ string) (esv1.SecretsClient, error) {
  167. return clientB, nil
  168. },
  169. verify: func(sc esv1.SecretsClient) {
  170. // we now must have this provider in the clientMap
  171. // and it mustbe the client defined in clientConstructor
  172. assert.NotNil(t, sc)
  173. c, ok := mgr.clientMap[provKey]
  174. assert.True(t, ok)
  175. assert.Same(t, c.client, clientB)
  176. },
  177. afterClose: func() {
  178. v, ok := mgr.clientMap[provKey]
  179. assert.False(t, ok)
  180. assert.True(t, clientB.closeCalled)
  181. assert.Nil(t, v)
  182. },
  183. },
  184. {
  185. name: "retrieve cached client when store matches",
  186. wantErr: false,
  187. fields: fields{
  188. client: fakeclient.NewClientBuilder().
  189. WithScheme(scheme).
  190. WithObjects(defaultStore).
  191. Build(),
  192. clientMap: map[clientKey]*clientVal{
  193. provKey: {
  194. client: clientA,
  195. store: defaultStore,
  196. },
  197. },
  198. },
  199. args: args{
  200. storeRef: esv1.SecretStoreRef{
  201. Name: defaultStore.Name,
  202. Kind: esv1.SecretStoreKind,
  203. },
  204. namespace: defaultStore.Namespace,
  205. sourceRef: nil,
  206. },
  207. clientConstructor: func(_ context.Context, _ esv1.GenericStore, _ client.Client, _ string) (esv1.SecretsClient, error) {
  208. // constructor should not be called,
  209. // the client from the cache should be returned instead
  210. t.Fail()
  211. return nil, nil
  212. },
  213. verify: func(sc esv1.SecretsClient) {
  214. // verify that the secretsClient is the one from cache
  215. assert.NotNil(t, sc)
  216. c, ok := mgr.clientMap[provKey]
  217. assert.True(t, ok)
  218. assert.Same(t, c.client, clientA)
  219. assert.Same(t, sc, clientA)
  220. },
  221. afterClose: func() {
  222. v, ok := mgr.clientMap[provKey]
  223. assert.False(t, ok)
  224. assert.True(t, clientA.closeCalled)
  225. assert.Nil(t, v)
  226. },
  227. },
  228. {
  229. name: "create new client when store doesn't match",
  230. wantErr: false,
  231. fields: fields{
  232. client: fakeclient.NewClientBuilder().
  233. WithScheme(scheme).
  234. WithObjects(otherStore).
  235. Build(),
  236. clientMap: map[clientKey]*clientVal{
  237. provKey: {
  238. // we have clientA in cache pointing at defaultStore
  239. client: clientA,
  240. store: defaultStore,
  241. },
  242. },
  243. },
  244. args: args{
  245. storeRef: esv1.SecretStoreRef{
  246. Name: otherStore.Name,
  247. Kind: esv1.SecretStoreKind,
  248. },
  249. namespace: otherStore.Namespace,
  250. sourceRef: nil,
  251. },
  252. clientConstructor: func(_ context.Context, _ esv1.GenericStore, _ client.Client, _ string) (esv1.SecretsClient, error) {
  253. // because there is a store mismatch
  254. // we create a new client
  255. return clientB, nil
  256. },
  257. verify: func(sc esv1.SecretsClient) {
  258. // verify that SecretsClient is NOT the one from cache
  259. assert.NotNil(t, sc)
  260. c, ok := mgr.clientMap[provKey]
  261. assert.True(t, ok)
  262. assert.Same(t, c.client, clientB)
  263. assert.Same(t, sc, clientB)
  264. assert.True(t, clientA.closeCalled)
  265. },
  266. afterClose: func() {
  267. v, ok := mgr.clientMap[provKey]
  268. assert.False(t, ok)
  269. assert.True(t, clientB.closeCalled)
  270. assert.Nil(t, v)
  271. },
  272. },
  273. }
  274. for _, tt := range tests {
  275. t.Run(tt.name, func(t *testing.T) {
  276. mgr = &Manager{
  277. log: logr.Discard(),
  278. client: tt.fields.client,
  279. enableFloodgate: true,
  280. clientMap: tt.fields.clientMap,
  281. }
  282. fakeProvider.newClientFunc = tt.clientConstructor
  283. clientA.closeCalled = false
  284. clientB.closeCalled = false
  285. got, err := mgr.Get(context.Background(), tt.args.storeRef, tt.args.namespace, tt.args.sourceRef)
  286. if (err != nil) != tt.wantErr {
  287. t.Errorf("Manager.Get() error = %v, wantErr %v", err, tt.wantErr)
  288. return
  289. }
  290. tt.verify(got)
  291. mgr.Close(context.Background())
  292. tt.afterClose()
  293. })
  294. }
  295. }
  296. func TestShouldProcessSecret(t *testing.T) {
  297. scheme := runtime.NewScheme()
  298. // add kubernetes schemes
  299. utilruntime.Must(clientgoscheme.AddToScheme(scheme))
  300. utilruntime.Must(apiextensionsv1.AddToScheme(scheme))
  301. // add external-secrets schemes
  302. utilruntime.Must(esv1.AddToScheme(scheme))
  303. testNamespace := "test-a"
  304. testCases := []struct {
  305. name string
  306. conditions []esv1.ClusterSecretStoreCondition
  307. namespace *corev1.Namespace
  308. wantErr string
  309. want bool
  310. }{
  311. {
  312. name: "processes a regex condition",
  313. conditions: []esv1.ClusterSecretStoreCondition{
  314. {
  315. NamespaceRegexes: []string{`test-*`},
  316. },
  317. },
  318. namespace: &corev1.Namespace{
  319. ObjectMeta: metav1.ObjectMeta{
  320. Name: testNamespace,
  321. },
  322. },
  323. want: true,
  324. },
  325. {
  326. name: "process multiple regexes",
  327. conditions: []esv1.ClusterSecretStoreCondition{
  328. {
  329. NamespaceRegexes: []string{`nope`, `test-*`},
  330. },
  331. },
  332. namespace: &corev1.Namespace{
  333. ObjectMeta: metav1.ObjectMeta{
  334. Name: testNamespace,
  335. },
  336. },
  337. want: true,
  338. },
  339. {
  340. name: "shouldn't process if nothing matches",
  341. conditions: []esv1.ClusterSecretStoreCondition{
  342. {
  343. NamespaceRegexes: []string{`nope`},
  344. },
  345. },
  346. namespace: &corev1.Namespace{
  347. ObjectMeta: metav1.ObjectMeta{
  348. Name: testNamespace,
  349. },
  350. },
  351. want: false,
  352. },
  353. }
  354. for _, tt := range testCases {
  355. t.Run(tt.name, func(t *testing.T) {
  356. fakeSpec := esv1.SecretStoreSpec{
  357. Conditions: tt.conditions,
  358. }
  359. defaultStore := &esv1.ClusterSecretStore{
  360. TypeMeta: metav1.TypeMeta{Kind: esv1.ClusterSecretStoreKind},
  361. ObjectMeta: metav1.ObjectMeta{
  362. Name: "foo",
  363. Namespace: tt.namespace.Name,
  364. },
  365. Spec: fakeSpec,
  366. }
  367. client := fakeclient.NewClientBuilder().WithScheme(scheme).WithObjects(defaultStore, tt.namespace).Build()
  368. clientMap := make(map[clientKey]*clientVal)
  369. mgr := &Manager{
  370. log: logr.Discard(),
  371. client: client,
  372. enableFloodgate: true,
  373. clientMap: clientMap,
  374. }
  375. got, err := mgr.shouldProcessSecret(defaultStore, tt.namespace.Name)
  376. require.NoError(t, err)
  377. assert.Equal(t, tt.want, got)
  378. })
  379. }
  380. }
  381. type WrapProvider struct {
  382. newClientFunc func(
  383. context.Context,
  384. esv1.GenericStore,
  385. client.Client,
  386. string) (esv1.SecretsClient, error)
  387. }
  388. // NewClient constructs a SecretsManager Provider.
  389. func (f *WrapProvider) NewClient(
  390. ctx context.Context,
  391. store esv1.GenericStore,
  392. kube client.Client,
  393. namespace string) (esv1.SecretsClient, error) {
  394. return f.newClientFunc(ctx, store, kube, namespace)
  395. }
  396. func (f *WrapProvider) Capabilities() esv1.SecretStoreCapabilities {
  397. return esv1.SecretStoreReadOnly
  398. }
  399. // ValidateStore checks if the provided store is valid.
  400. func (f *WrapProvider) ValidateStore(_ esv1.GenericStore) (admission.Warnings, error) {
  401. return nil, nil
  402. }
  403. type MockFakeClient struct {
  404. id string
  405. closeCalled bool
  406. }
  407. func (c *MockFakeClient) PushSecret(_ context.Context, _ *corev1.Secret, _ esv1.PushSecretData) error {
  408. return nil
  409. }
  410. func (c *MockFakeClient) DeleteSecret(_ context.Context, _ esv1.PushSecretRemoteRef) error {
  411. return nil
  412. }
  413. func (c *MockFakeClient) SecretExists(_ context.Context, _ esv1.PushSecretRemoteRef) (bool, error) {
  414. return false, nil
  415. }
  416. func (c *MockFakeClient) GetSecret(_ context.Context, _ esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
  417. return nil, nil
  418. }
  419. func (c *MockFakeClient) Validate() (esv1.ValidationResult, error) {
  420. return esv1.ValidationResultReady, nil
  421. }
  422. // GetSecretMap returns multiple k/v pairs from the provider.
  423. func (c *MockFakeClient) GetSecretMap(_ context.Context, _ esv1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
  424. return nil, nil
  425. }
  426. // GetAllSecrets returns multiple k/v pairs from the provider.
  427. func (c *MockFakeClient) GetAllSecrets(_ context.Context, _ esv1.ExternalSecretFind) (map[string][]byte, error) {
  428. return nil, nil
  429. }
  430. func (c *MockFakeClient) Close(_ context.Context) error {
  431. c.closeCalled = true
  432. return nil
  433. }