clusterexternalsecret_controller_test.go 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  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 clusterexternalsecret
  13. import (
  14. "context"
  15. "math/rand"
  16. "time"
  17. . "github.com/onsi/ginkgo/v2"
  18. . "github.com/onsi/gomega"
  19. v1 "k8s.io/api/core/v1"
  20. apierrors "k8s.io/apimachinery/pkg/api/errors"
  21. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  22. "k8s.io/apimachinery/pkg/types"
  23. "sigs.k8s.io/controller-runtime/pkg/client"
  24. esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
  25. ctest "github.com/external-secrets/external-secrets/pkg/controllers/commontest"
  26. )
  27. var (
  28. timeout = time.Second * 10
  29. interval = time.Millisecond * 250
  30. )
  31. var letterRunes = []rune("abcdefghijklmnopqrstuvwxyz")
  32. func RandString(n int) string {
  33. b := make([]rune, n)
  34. for i := range b {
  35. b[i] = letterRunes[rand.Intn(len(letterRunes))]
  36. }
  37. return string(b)
  38. }
  39. type testNamespace struct {
  40. namespace v1.Namespace
  41. containsES bool
  42. deletedES bool
  43. }
  44. type testCase struct {
  45. clusterExternalSecret *esv1beta1.ClusterExternalSecret
  46. // These are the namespaces that are being tested
  47. externalSecretNamespaces []testNamespace
  48. // The labels to be used for the namespaces
  49. namespaceLabels map[string]string
  50. // This is a setup function called for each test much like BeforeEach but with knowledge of the test case
  51. // This is used by default to create namespaces and random labels
  52. setup func(*testCase)
  53. // Is a method that's ran after everything has been created, but before the check methods are called
  54. beforeCheck func(*testCase)
  55. // A function to do any work needed before a test is ran
  56. preTest func()
  57. // checkCondition should return true if the externalSecret
  58. // has the expected condition
  59. checkCondition func(*esv1beta1.ClusterExternalSecret) bool
  60. // checkExternalSecret is called after the condition has been verified
  61. // use this to verify the externalSecret
  62. checkClusterExternalSecret func(*esv1beta1.ClusterExternalSecret)
  63. // checkExternalSecret is called after the condition has been verified
  64. // use this to verify the externalSecret
  65. checkExternalSecret func(*esv1beta1.ClusterExternalSecret, *esv1beta1.ExternalSecret)
  66. }
  67. type testTweaks func(*testCase)
  68. var _ = Describe("ClusterExternalSecret controller", func() {
  69. const (
  70. ClusterExternalSecretName = "test-ces"
  71. ExternalSecretName = "test-es"
  72. ExternalSecretStore = "test-store"
  73. ExternalSecretTargetSecretName = "test-secret"
  74. ClusterSecretStoreNamespace = "css-test-ns"
  75. FakeManager = "fake.manager"
  76. FooValue = "map-foo-value"
  77. BarValue = "map-bar-value"
  78. )
  79. var ExternalSecretNamespaceTargets = []testNamespace{
  80. {
  81. namespace: v1.Namespace{
  82. ObjectMeta: metav1.ObjectMeta{
  83. Name: "test-ns-1",
  84. },
  85. },
  86. containsES: true,
  87. },
  88. {
  89. namespace: v1.Namespace{
  90. ObjectMeta: metav1.ObjectMeta{
  91. Name: "test-ns-2",
  92. },
  93. },
  94. containsES: true,
  95. },
  96. {
  97. namespace: v1.Namespace{
  98. ObjectMeta: metav1.ObjectMeta{
  99. Name: "test-ns-5",
  100. },
  101. },
  102. containsES: false,
  103. },
  104. }
  105. const targetProp = "targetProperty"
  106. const remoteKey = "barz"
  107. const remoteProperty = "bang"
  108. makeDefaultTestCase := func() *testCase {
  109. return &testCase{
  110. checkCondition: func(ces *esv1beta1.ClusterExternalSecret) bool {
  111. cond := GetClusterExternalSecretCondition(ces.Status, esv1beta1.ClusterExternalSecretReady)
  112. if cond == nil || cond.Status != v1.ConditionTrue {
  113. return false
  114. }
  115. return true
  116. },
  117. checkClusterExternalSecret: func(es *esv1beta1.ClusterExternalSecret) {
  118. // To be implemented by the tests
  119. },
  120. checkExternalSecret: func(*esv1beta1.ClusterExternalSecret, *esv1beta1.ExternalSecret) {
  121. // To be implemented by the tests
  122. },
  123. clusterExternalSecret: &esv1beta1.ClusterExternalSecret{
  124. ObjectMeta: metav1.ObjectMeta{
  125. GenerateName: ClusterExternalSecretName,
  126. },
  127. Spec: esv1beta1.ClusterExternalSecretSpec{
  128. NamespaceSelector: metav1.LabelSelector{},
  129. ExternalSecretName: ExternalSecretName,
  130. ExternalSecretSpec: esv1beta1.ExternalSecretSpec{
  131. SecretStoreRef: esv1beta1.SecretStoreRef{
  132. Name: ExternalSecretStore,
  133. },
  134. Target: esv1beta1.ExternalSecretTarget{
  135. Name: ExternalSecretTargetSecretName,
  136. },
  137. Data: []esv1beta1.ExternalSecretData{
  138. {
  139. SecretKey: targetProp,
  140. RemoteRef: esv1beta1.ExternalSecretDataRemoteRef{
  141. Key: remoteKey,
  142. Property: remoteProperty,
  143. },
  144. },
  145. },
  146. },
  147. },
  148. },
  149. setup: func(tc *testCase) {
  150. // Generate a random label since we don't want to match previous ones.
  151. tc.namespaceLabels = map[string]string{
  152. RandString(5): RandString(5),
  153. }
  154. namespaces := []testNamespace{}
  155. for _, ns := range ExternalSecretNamespaceTargets {
  156. name, err := ctest.CreateNamespaceWithLabels(ns.namespace.Name, k8sClient, tc.namespaceLabels)
  157. Expect(err).ToNot(HaveOccurred())
  158. newNs := ns
  159. newNs.namespace.ObjectMeta.Name = name
  160. namespaces = append(namespaces, newNs)
  161. }
  162. tc.externalSecretNamespaces = namespaces
  163. tc.clusterExternalSecret.Spec.NamespaceSelector.MatchLabels = tc.namespaceLabels
  164. },
  165. }
  166. }
  167. // If the ES does noes not have a name specified then it should use the CES name
  168. syncWithoutESName := func(tc *testCase) {
  169. tc.clusterExternalSecret.Spec.ExternalSecretName = ""
  170. tc.checkExternalSecret = func(ces *esv1beta1.ClusterExternalSecret, es *esv1beta1.ExternalSecret) {
  171. Expect(es.ObjectMeta.Name).To(Equal(ces.ObjectMeta.Name))
  172. }
  173. }
  174. doNotOverwriteExistingES := func(tc *testCase) {
  175. tc.preTest = func() {
  176. es := &esv1beta1.ExternalSecret{
  177. ObjectMeta: metav1.ObjectMeta{
  178. Name: ExternalSecretName,
  179. Namespace: tc.externalSecretNamespaces[0].namespace.Name,
  180. },
  181. }
  182. err := k8sClient.Create(context.Background(), es, &client.CreateOptions{})
  183. Expect(err).ShouldNot(HaveOccurred())
  184. }
  185. tc.checkCondition = func(ces *esv1beta1.ClusterExternalSecret) bool {
  186. cond := GetClusterExternalSecretCondition(ces.Status, esv1beta1.ClusterExternalSecretPartiallyReady)
  187. return cond != nil
  188. }
  189. tc.checkClusterExternalSecret = func(ces *esv1beta1.ClusterExternalSecret) {
  190. Expect(len(ces.Status.FailedNamespaces)).Should(Equal(1))
  191. failure := ces.Status.FailedNamespaces[0]
  192. Expect(failure.Namespace).Should(Equal(tc.externalSecretNamespaces[0].namespace.Name))
  193. Expect(failure.Reason).Should(Equal(errSecretAlreadyExists))
  194. }
  195. }
  196. populatedProvisionedNamespaces := func(tc *testCase) {
  197. tc.checkClusterExternalSecret = func(ces *esv1beta1.ClusterExternalSecret) {
  198. for _, namespace := range tc.externalSecretNamespaces {
  199. if !namespace.containsES {
  200. continue
  201. }
  202. Expect(sliceContainsString(namespace.namespace.Name, ces.Status.ProvisionedNamespaces)).To(BeTrue())
  203. }
  204. }
  205. }
  206. deleteESInNonMatchingNS := func(tc *testCase) {
  207. tc.beforeCheck = func(tc *testCase) {
  208. ns := tc.externalSecretNamespaces[0]
  209. // Remove the labels, but leave the should contain ES so we can still check it
  210. ns.namespace.ObjectMeta.Labels = map[string]string{}
  211. tc.externalSecretNamespaces[0].deletedES = true
  212. err := k8sClient.Update(context.Background(), &ns.namespace, &client.UpdateOptions{})
  213. Expect(err).ToNot(HaveOccurred())
  214. time.Sleep(time.Second) // Sleep to make sure the controller gets it.
  215. }
  216. }
  217. DescribeTable("When reconciling a ClusterExternal Secret",
  218. func(tweaks ...testTweaks) {
  219. tc := makeDefaultTestCase()
  220. for _, tweak := range tweaks {
  221. tweak(tc)
  222. }
  223. // Run test setup
  224. tc.setup(tc)
  225. if tc.preTest != nil {
  226. By("running pre-test")
  227. tc.preTest()
  228. }
  229. ctx := context.Background()
  230. By("creating namespaces and cluster external secret")
  231. err := k8sClient.Create(ctx, tc.clusterExternalSecret)
  232. Expect(err).ShouldNot(HaveOccurred())
  233. cesKey := types.NamespacedName{Name: tc.clusterExternalSecret.Name}
  234. createdCES := &esv1beta1.ClusterExternalSecret{}
  235. By("checking the ces condition")
  236. Eventually(func() bool {
  237. err := k8sClient.Get(ctx, cesKey, createdCES)
  238. if err != nil {
  239. return false
  240. }
  241. return tc.checkCondition(createdCES)
  242. }, timeout, interval).Should(BeTrue())
  243. // Run before check
  244. if tc.beforeCheck != nil {
  245. tc.beforeCheck(tc)
  246. }
  247. tc.checkClusterExternalSecret(createdCES)
  248. if tc.checkExternalSecret != nil {
  249. for _, ns := range tc.externalSecretNamespaces {
  250. if !ns.containsES {
  251. continue
  252. }
  253. es := &esv1beta1.ExternalSecret{}
  254. esName := createdCES.Spec.ExternalSecretName
  255. if esName == "" {
  256. esName = createdCES.ObjectMeta.Name
  257. }
  258. esLookupKey := types.NamespacedName{
  259. Name: esName,
  260. Namespace: ns.namespace.Name,
  261. }
  262. Eventually(func() bool {
  263. err := k8sClient.Get(ctx, esLookupKey, es)
  264. if ns.deletedES && apierrors.IsNotFound(err) {
  265. return true
  266. }
  267. return err == nil
  268. }, timeout, interval).Should(BeTrue())
  269. tc.checkExternalSecret(createdCES, es)
  270. }
  271. }
  272. },
  273. Entry("Should use cluster external secret name if external secret name isn't defined", syncWithoutESName),
  274. Entry("Should not overwrite existing external secrets and error out if one is present", doNotOverwriteExistingES),
  275. Entry("Should have list of all provisioned namespaces", populatedProvisionedNamespaces),
  276. Entry("Should delete external secrets when namespaces no longer match", deleteESInNonMatchingNS))
  277. })
  278. func sliceContainsString(toFind string, collection []string) bool {
  279. for _, val := range collection {
  280. if val == toFind {
  281. return true
  282. }
  283. }
  284. return false
  285. }