common_test.go 16 KB

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