common_test.go 17 KB

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