manager_test.go 12 KB

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