common_test.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549
  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. "time"
  17. corev1 "k8s.io/api/core/v1"
  18. apierrors "k8s.io/apimachinery/pkg/api/errors"
  19. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  20. "k8s.io/apimachinery/pkg/types"
  21. esapi "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  22. esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
  23. . "github.com/onsi/ginkgo/v2"
  24. . "github.com/onsi/gomega"
  25. )
  26. type testCase struct {
  27. store esapi.GenericStore
  28. ps *esv1alpha1.PushSecret
  29. assert func()
  30. }
  31. const (
  32. defaultStoreName = "default-store"
  33. defaultControllerClass = "test-ctrl"
  34. )
  35. var _ = Describe("SecretStore Controller", func() {
  36. var test *testCase
  37. BeforeEach(func() {
  38. test = makeDefaultTestcase()
  39. })
  40. Context("Reconcile Logic", func() {
  41. AfterEach(func() {
  42. Expect(k8sClient.Delete(context.Background(), test.store)).ToNot(HaveOccurred())
  43. })
  44. // an invalid provider config should be reflected
  45. // in the store status condition
  46. invalidProvider := func(tc *testCase) {
  47. tc.assert = func() {
  48. Eventually(func() bool {
  49. ss := tc.store.Copy()
  50. err := k8sClient.Get(context.Background(), types.NamespacedName{
  51. Name: defaultStoreName,
  52. Namespace: ss.GetObjectMeta().Namespace,
  53. }, ss)
  54. if err != nil {
  55. return false
  56. }
  57. status := ss.GetStatus()
  58. if len(status.Conditions) != 1 {
  59. return false
  60. }
  61. return status.Conditions[0].Reason == esapi.ReasonInvalidProviderConfig &&
  62. hasEvent(tc.store.GetTypeMeta().Kind, ss.GetName(), esapi.ReasonInvalidProviderConfig)
  63. }).
  64. WithTimeout(time.Second * 10).
  65. WithPolling(time.Second).
  66. Should(BeTrue())
  67. }
  68. }
  69. // if controllerClass does not match the controller
  70. // should not touch this store
  71. ignoreControllerClass := func(tc *testCase) {
  72. spc := tc.store.GetSpec()
  73. spc.Controller = "something-else"
  74. tc.assert = func() {
  75. Consistently(func() bool {
  76. ss := tc.store.Copy()
  77. err := k8sClient.Get(context.Background(), types.NamespacedName{
  78. Name: defaultStoreName,
  79. Namespace: ss.GetObjectMeta().Namespace,
  80. }, ss)
  81. if err != nil {
  82. return true
  83. }
  84. conditionLen := len(ss.GetStatus().Conditions) == 0
  85. if !conditionLen {
  86. GinkgoLogr.Info("store conditions is NOT empty but should have been", "conditions", ss.GetStatus().Conditions)
  87. }
  88. return conditionLen
  89. }).
  90. WithTimeout(time.Second*3).
  91. WithPolling(time.Millisecond*500).
  92. Should(BeTrue(), "condition should have been empty")
  93. }
  94. }
  95. validProvider := func(tc *testCase) {
  96. spc := tc.store.GetSpec()
  97. spc.Provider.Vault = nil
  98. spc.Provider.Fake = &esapi.FakeProvider{
  99. Data: []esapi.FakeProviderData{},
  100. }
  101. tc.assert = func() {
  102. Eventually(func() bool {
  103. ss := tc.store.Copy()
  104. err := k8sClient.Get(context.Background(), types.NamespacedName{
  105. Name: defaultStoreName,
  106. Namespace: ss.GetNamespace(),
  107. }, ss)
  108. if err != nil {
  109. return false
  110. }
  111. if len(ss.GetStatus().Conditions) != 1 {
  112. return false
  113. }
  114. return ss.GetStatus().Conditions[0].Reason == esapi.ReasonStoreValid &&
  115. ss.GetStatus().Conditions[0].Type == esapi.SecretStoreReady &&
  116. ss.GetStatus().Conditions[0].Status == corev1.ConditionTrue &&
  117. hasEvent(tc.store.GetTypeMeta().Kind, ss.GetName(), esapi.ReasonStoreValid)
  118. }).
  119. WithTimeout(time.Second * 10).
  120. WithPolling(time.Second).
  121. Should(BeTrue())
  122. }
  123. }
  124. readWrite := func(tc *testCase) {
  125. spc := tc.store.GetSpec()
  126. spc.Provider.Vault = nil
  127. spc.Provider.Fake = &esapi.FakeProvider{
  128. Data: []esapi.FakeProviderData{},
  129. }
  130. tc.assert = func() {
  131. Eventually(func() bool {
  132. ss := tc.store.Copy()
  133. err := k8sClient.Get(context.Background(), types.NamespacedName{
  134. Name: defaultStoreName,
  135. Namespace: ss.GetNamespace(),
  136. }, ss)
  137. if err != nil {
  138. return false
  139. }
  140. if ss.GetStatus().Capabilities != esapi.SecretStoreReadWrite {
  141. return false
  142. }
  143. return true
  144. }).
  145. WithTimeout(time.Second * 10).
  146. WithPolling(time.Second).
  147. Should(BeTrue())
  148. }
  149. }
  150. // an unknown store validation result should be reflected
  151. // in the store status condition
  152. validationUnknown := func(tc *testCase) {
  153. spc := tc.store.GetSpec()
  154. spc.Provider.Vault = nil
  155. validationResultUnknown := esapi.ValidationResultUnknown
  156. spc.Provider.Fake = &esapi.FakeProvider{
  157. Data: []esapi.FakeProviderData{},
  158. ValidationResult: &validationResultUnknown,
  159. }
  160. tc.assert = func() {
  161. Eventually(func() bool {
  162. ss := tc.store.Copy()
  163. err := k8sClient.Get(context.Background(), types.NamespacedName{
  164. Name: defaultStoreName,
  165. Namespace: ss.GetNamespace(),
  166. }, ss)
  167. if err != nil {
  168. return false
  169. }
  170. if len(ss.GetStatus().Conditions) != 1 {
  171. return false
  172. }
  173. return ss.GetStatus().Conditions[0].Reason == esapi.ReasonValidationUnknown &&
  174. ss.GetStatus().Conditions[0].Type == esapi.SecretStoreReady &&
  175. ss.GetStatus().Conditions[0].Status == corev1.ConditionTrue &&
  176. hasEvent(tc.store.GetTypeMeta().Kind, ss.GetName(), esapi.ReasonValidationUnknown)
  177. }).
  178. WithTimeout(time.Second * 5).
  179. WithPolling(time.Second).
  180. Should(BeTrue())
  181. }
  182. }
  183. DescribeTable("Provider Configuration", func(muts ...func(tc *testCase)) {
  184. for _, mut := range muts {
  185. mut(test)
  186. }
  187. err := k8sClient.Create(context.Background(), test.store.Copy())
  188. Expect(err).ToNot(HaveOccurred())
  189. test.assert()
  190. },
  191. // Namespaced store tests
  192. Entry("[namespace] invalid provider should set InvalidStore condition", invalidProvider),
  193. Entry("[namespace] should ignore stores with non-matching controller class", ignoreControllerClass),
  194. Entry("[namespace] valid provider should have status=ready", validProvider),
  195. Entry("[namespace] valid provider should have capabilities=ReadWrite", readWrite),
  196. Entry("[cluster] validation unknown status should set ValidationUnknown condition", validationUnknown),
  197. // Cluster store tests
  198. Entry("[cluster] invalid provider should set InvalidStore condition", invalidProvider, useClusterStore),
  199. Entry("[cluster] should ignore stores with non-matching controller class", ignoreControllerClass, useClusterStore),
  200. Entry("[cluster] valid provider should have status=ready", validProvider, useClusterStore),
  201. Entry("[cluster] valid provider should have capabilities=ReadWrite", readWrite, useClusterStore),
  202. Entry("[cluster] validation unknown status should set ValidationUnknown condition", validationUnknown, useClusterStore),
  203. )
  204. })
  205. Context("Finalizer Management", func() {
  206. BeforeEach(func() {
  207. // Setup valid provider for finalizer tests
  208. spc := test.store.GetSpec()
  209. spc.Provider.Vault = nil
  210. spc.Provider.Fake = &esapi.FakeProvider{
  211. Data: []esapi.FakeProviderData{},
  212. }
  213. })
  214. AfterEach(func() {
  215. cleanupResources(test)
  216. })
  217. DescribeTable("Finalizer Addition", func(muts ...func(tc *testCase)) {
  218. for _, mut := range muts {
  219. mut(test)
  220. }
  221. Expect(k8sClient.Create(context.Background(), test.store)).ToNot(HaveOccurred())
  222. Expect(k8sClient.Create(context.Background(), test.ps)).ToNot(HaveOccurred())
  223. Eventually(func() []string {
  224. return getStoreFinalizers(test.store)
  225. }).
  226. WithTimeout(time.Second * 10).
  227. WithPolling(time.Second).
  228. Should(ContainElement(secretStoreFinalizer))
  229. },
  230. Entry("[namespace] should add finalizer when PushSecret with DeletionPolicy=Delete is created", usePushSecret),
  231. Entry("[cluster] should add finalizer when PushSecret with DeletionPolicy=Delete is created", usePushSecret, useClusterStore),
  232. )
  233. DescribeTable("Finalizer Removal on PushSecret Deletion", func(muts ...func(tc *testCase)) {
  234. for _, mut := range muts {
  235. mut(test)
  236. }
  237. test.store.SetFinalizers([]string{secretStoreFinalizer})
  238. Expect(k8sClient.Create(context.Background(), test.store)).ToNot(HaveOccurred())
  239. Expect(k8sClient.Create(context.Background(), test.ps)).ToNot(HaveOccurred())
  240. Expect(k8sClient.Delete(context.Background(), test.ps)).ToNot(HaveOccurred())
  241. Eventually(func() []string {
  242. return getStoreFinalizers(test.store)
  243. }).
  244. WithTimeout(time.Second * 10).
  245. WithPolling(time.Second).
  246. ShouldNot(ContainElement(secretStoreFinalizer))
  247. },
  248. Entry("[namespace] should remove finalizer when PushSecret is deleted", usePushSecret),
  249. Entry("[cluster] should remove finalizer when PushSecret is deleted", usePushSecret, useClusterStore),
  250. )
  251. DescribeTable("Store Deletion Prevention", func(muts ...func(tc *testCase)) {
  252. for _, mut := range muts {
  253. mut(test)
  254. }
  255. Expect(k8sClient.Create(context.Background(), test.store)).ToNot(HaveOccurred())
  256. Expect(k8sClient.Create(context.Background(), test.ps)).ToNot(HaveOccurred())
  257. // Wait for finalizer to be added
  258. Eventually(func() []string {
  259. return getStoreFinalizers(test.store)
  260. }).
  261. WithTimeout(time.Second * 10).
  262. WithPolling(time.Second).
  263. Should(ContainElement(secretStoreFinalizer))
  264. Expect(k8sClient.Delete(context.Background(), test.store)).ToNot(HaveOccurred())
  265. Consistently(func() []string {
  266. return getStoreFinalizers(test.store)
  267. }).
  268. WithTimeout(time.Second * 3).
  269. WithPolling(time.Millisecond * 500).
  270. Should(ContainElement(secretStoreFinalizer))
  271. },
  272. Entry("[namespace] should prevent deletion when finalizer exists", usePushSecret),
  273. Entry("[cluster] should prevent deletion when finalizer exists", usePushSecret, useClusterStore),
  274. )
  275. DescribeTable("Complete Deletion Flow", func(muts ...func(tc *testCase)) {
  276. for _, mut := range muts {
  277. mut(test)
  278. }
  279. Expect(k8sClient.Create(context.Background(), test.store)).ToNot(HaveOccurred())
  280. Expect(k8sClient.Create(context.Background(), test.ps)).ToNot(HaveOccurred())
  281. Expect(k8sClient.Delete(context.Background(), test.store)).ToNot(HaveOccurred())
  282. Expect(k8sClient.Delete(context.Background(), test.ps)).ToNot(HaveOccurred())
  283. Eventually(func() bool {
  284. err := k8sClient.Get(context.Background(), types.NamespacedName{
  285. Name: test.store.GetName(),
  286. Namespace: test.store.GetNamespace(),
  287. }, test.store)
  288. return apierrors.IsNotFound(err)
  289. }).
  290. WithTimeout(time.Second * 10).
  291. WithPolling(time.Second).
  292. Should(BeTrue())
  293. },
  294. Entry("[namespace] should allow deletion when both Store and PushSecret are deleted", usePushSecret),
  295. Entry("[cluster] should allow deletion when both Store and PushSecret are deleted", usePushSecret, useClusterStore),
  296. )
  297. DescribeTable("Multiple PushSecrets Scenario", func(muts ...func(tc *testCase)) {
  298. for _, mut := range muts {
  299. mut(test)
  300. }
  301. ps2 := test.ps.DeepCopy()
  302. ps2.Name = "push-secret-2"
  303. Expect(k8sClient.Create(context.Background(), test.store)).ToNot(HaveOccurred())
  304. Expect(k8sClient.Create(context.Background(), test.ps)).ToNot(HaveOccurred())
  305. Expect(k8sClient.Create(context.Background(), ps2)).ToNot(HaveOccurred())
  306. // Wait for finalizer to be added
  307. Eventually(func() []string {
  308. return getStoreFinalizers(test.store)
  309. }).
  310. WithTimeout(time.Second * 10).
  311. WithPolling(time.Second).
  312. Should(ContainElement(secretStoreFinalizer))
  313. Expect(k8sClient.Delete(context.Background(), test.ps)).ToNot(HaveOccurred())
  314. // Finalizer should remain because ps2 still exists
  315. Consistently(func() []string {
  316. return getStoreFinalizers(test.store)
  317. }).
  318. WithTimeout(time.Second * 3).
  319. WithPolling(time.Millisecond * 500).
  320. Should(ContainElement(secretStoreFinalizer))
  321. // Cleanup
  322. Expect(k8sClient.Delete(context.Background(), ps2)).ToNot(HaveOccurred())
  323. },
  324. Entry("[namespace] finalizer should remain when other PushSecrets exist", usePushSecret),
  325. Entry("[cluster] finalizer should remain when other PushSecrets exist", usePushSecret, useClusterStore),
  326. )
  327. DescribeTable("DeletionPolicy Change", func(muts ...func(tc *testCase)) {
  328. for _, mut := range muts {
  329. mut(test)
  330. }
  331. Expect(k8sClient.Create(context.Background(), test.store)).ToNot(HaveOccurred())
  332. Expect(k8sClient.Create(context.Background(), test.ps)).ToNot(HaveOccurred())
  333. // Wait for finalizer to be added
  334. Eventually(func() []string {
  335. return getStoreFinalizers(test.store)
  336. }).
  337. WithTimeout(time.Second * 10).
  338. WithPolling(time.Second).
  339. Should(ContainElement(secretStoreFinalizer))
  340. // Update PushSecret to DeletionPolicy=None
  341. Eventually(func() error {
  342. err := k8sClient.Get(context.Background(), types.NamespacedName{
  343. Name: test.ps.Name,
  344. Namespace: test.ps.Namespace,
  345. }, test.ps)
  346. Expect(err).ToNot(HaveOccurred())
  347. test.ps.Spec.DeletionPolicy = esv1alpha1.PushSecretDeletionPolicyNone
  348. return k8sClient.Update(context.Background(), test.ps)
  349. }).
  350. WithTimeout(time.Second * 10).
  351. WithPolling(time.Second).
  352. Should(Succeed())
  353. Eventually(func() []string {
  354. return getStoreFinalizers(test.store)
  355. }).
  356. WithTimeout(time.Second * 10).
  357. WithPolling(time.Second).
  358. ShouldNot(ContainElement(secretStoreFinalizer))
  359. },
  360. Entry("[namespace] should remove finalizer when DeletionPolicy changes to None", usePushSecret),
  361. Entry("[cluster] should remove finalizer when DeletionPolicy changes to None", usePushSecret, useClusterStore),
  362. )
  363. })
  364. })
  365. func cleanupResources(test *testCase) {
  366. if test.ps != nil {
  367. err := k8sClient.Delete(context.Background(), test.ps)
  368. if err != nil && !apierrors.IsNotFound(err) {
  369. Expect(err).ToNot(HaveOccurred())
  370. }
  371. }
  372. err := k8sClient.Delete(context.Background(), test.store)
  373. if err != nil && !apierrors.IsNotFound(err) {
  374. Expect(err).ToNot(HaveOccurred())
  375. }
  376. Eventually(func() bool {
  377. err := k8sClient.Get(context.Background(), types.NamespacedName{
  378. Name: test.store.GetName(),
  379. Namespace: test.store.GetNamespace(),
  380. }, test.store)
  381. return apierrors.IsNotFound(err)
  382. }).
  383. WithTimeout(time.Second * 10).
  384. WithPolling(time.Second).
  385. Should(BeTrue())
  386. }
  387. func getStoreFinalizers(store esapi.GenericStore) []string {
  388. err := k8sClient.Get(context.Background(), types.NamespacedName{
  389. Name: store.GetName(),
  390. Namespace: store.GetNamespace(),
  391. }, store)
  392. if err != nil {
  393. return []string{}
  394. }
  395. return store.GetFinalizers()
  396. }
  397. func makeDefaultTestcase() *testCase {
  398. return &testCase{
  399. assert: func() {
  400. // this is a noop by default
  401. },
  402. store: &esapi.SecretStore{
  403. TypeMeta: metav1.TypeMeta{
  404. Kind: esapi.SecretStoreKind,
  405. APIVersion: esapi.SecretStoreKindAPIVersion,
  406. },
  407. ObjectMeta: metav1.ObjectMeta{
  408. Name: defaultStoreName,
  409. Namespace: "default",
  410. },
  411. Spec: esapi.SecretStoreSpec{
  412. Controller: defaultControllerClass,
  413. // empty provider
  414. // a testCase mutator must fill in the concrete provider
  415. Provider: &esapi.SecretStoreProvider{
  416. Vault: &esapi.VaultProvider{
  417. Version: esapi.VaultKVStoreV1,
  418. },
  419. },
  420. },
  421. },
  422. }
  423. }
  424. func useClusterStore(tc *testCase) {
  425. spc := tc.store.GetSpec()
  426. meta := tc.store.GetObjectMeta()
  427. tc.store = &esapi.ClusterSecretStore{
  428. TypeMeta: metav1.TypeMeta{
  429. Kind: esapi.ClusterSecretStoreKind,
  430. APIVersion: esapi.ClusterSecretStoreKindAPIVersion,
  431. },
  432. ObjectMeta: metav1.ObjectMeta{
  433. Name: meta.Name,
  434. },
  435. Spec: *spc,
  436. }
  437. if tc.ps != nil {
  438. tc.ps.Spec.SecretStoreRefs[0].Kind = esapi.ClusterSecretStoreKind
  439. }
  440. }
  441. func usePushSecret(tc *testCase) {
  442. tc.ps = &esv1alpha1.PushSecret{
  443. ObjectMeta: metav1.ObjectMeta{
  444. Name: "push-secret",
  445. Namespace: "default",
  446. },
  447. Spec: esv1alpha1.PushSecretSpec{
  448. DeletionPolicy: esv1alpha1.PushSecretDeletionPolicyDelete,
  449. Selector: esv1alpha1.PushSecretSelector{
  450. Secret: &esv1alpha1.PushSecretSecret{
  451. Name: "foo",
  452. },
  453. },
  454. SecretStoreRefs: []esv1alpha1.PushSecretStoreRef{
  455. {
  456. Name: defaultStoreName,
  457. },
  458. },
  459. },
  460. }
  461. }
  462. func hasEvent(involvedKind, name, reason string) bool {
  463. el := &corev1.EventList{}
  464. err := k8sClient.List(context.Background(), el)
  465. if err != nil {
  466. return false
  467. }
  468. for i := range el.Items {
  469. ev := el.Items[i]
  470. if ev.InvolvedObject.Kind == involvedKind && ev.InvolvedObject.Name == name {
  471. if ev.Reason == reason {
  472. return true
  473. }
  474. }
  475. }
  476. return false
  477. }