common_test.go 16 KB

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