clusterexternalsecret_controller_test.go 12 KB

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