externalsecret_controller_manifest_test.go 30 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154
  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 externalsecret
  14. import (
  15. "context"
  16. "testing"
  17. "github.com/go-logr/logr"
  18. "github.com/stretchr/testify/assert"
  19. "github.com/stretchr/testify/require"
  20. v1 "k8s.io/api/core/v1"
  21. apierrors "k8s.io/apimachinery/pkg/api/errors"
  22. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  23. "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
  24. "k8s.io/apimachinery/pkg/runtime/schema"
  25. "k8s.io/apimachinery/pkg/types"
  26. "k8s.io/client-go/kubernetes/scheme"
  27. ctrl "sigs.k8s.io/controller-runtime"
  28. fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
  29. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  30. "github.com/external-secrets/external-secrets/runtime/esutils"
  31. )
  32. func TestIsGenericTarget(t *testing.T) {
  33. tests := []struct {
  34. name string
  35. es *esv1.ExternalSecret
  36. expected bool
  37. }{
  38. {
  39. name: "nil manifest - Secret target",
  40. es: &esv1.ExternalSecret{
  41. Spec: esv1.ExternalSecretSpec{
  42. Target: esv1.ExternalSecretTarget{
  43. Manifest: nil,
  44. },
  45. },
  46. },
  47. expected: false,
  48. },
  49. {
  50. name: "ConfigMap manifest target",
  51. es: &esv1.ExternalSecret{
  52. Spec: esv1.ExternalSecretSpec{
  53. Target: esv1.ExternalSecretTarget{
  54. Manifest: &esv1.ManifestReference{
  55. APIVersion: "v1",
  56. Kind: "ConfigMap",
  57. },
  58. },
  59. },
  60. },
  61. expected: true,
  62. },
  63. {
  64. name: "Custom Resource manifest target",
  65. es: &esv1.ExternalSecret{
  66. Spec: esv1.ExternalSecretSpec{
  67. Target: esv1.ExternalSecretTarget{
  68. Manifest: &esv1.ManifestReference{
  69. APIVersion: "argoproj.io/v1alpha1",
  70. Kind: "Application",
  71. },
  72. },
  73. },
  74. },
  75. expected: true,
  76. },
  77. }
  78. for _, tt := range tests {
  79. t.Run(tt.name, func(t *testing.T) {
  80. result := isGenericTarget(tt.es)
  81. assert.Equal(t, tt.expected, result)
  82. })
  83. }
  84. }
  85. func TestValidateGenericTarget(t *testing.T) {
  86. tests := []struct {
  87. name string
  88. es *esv1.ExternalSecret
  89. allowGenericTargets bool
  90. expectedError bool
  91. errorContains string
  92. }{
  93. {
  94. name: "ConfigMap target - flag enabled - valid",
  95. es: &esv1.ExternalSecret{
  96. Spec: esv1.ExternalSecretSpec{
  97. Target: esv1.ExternalSecretTarget{
  98. Manifest: &esv1.ManifestReference{
  99. APIVersion: "v1",
  100. Kind: "ConfigMap",
  101. },
  102. },
  103. },
  104. },
  105. allowGenericTargets: true,
  106. expectedError: false,
  107. },
  108. {
  109. name: "ConfigMap target - flag disabled",
  110. es: &esv1.ExternalSecret{
  111. Spec: esv1.ExternalSecretSpec{
  112. Target: esv1.ExternalSecretTarget{
  113. Manifest: &esv1.ManifestReference{
  114. APIVersion: "v1",
  115. Kind: "ConfigMap",
  116. },
  117. },
  118. },
  119. },
  120. allowGenericTargets: false,
  121. expectedError: true,
  122. errorContains: "generic targets are disabled",
  123. },
  124. {
  125. name: "Missing APIVersion",
  126. es: &esv1.ExternalSecret{
  127. Spec: esv1.ExternalSecretSpec{
  128. Target: esv1.ExternalSecretTarget{
  129. Manifest: &esv1.ManifestReference{
  130. APIVersion: "",
  131. Kind: "ConfigMap",
  132. },
  133. },
  134. },
  135. },
  136. allowGenericTargets: true,
  137. expectedError: true,
  138. errorContains: "apiVersion is required",
  139. },
  140. {
  141. name: "Missing Kind",
  142. es: &esv1.ExternalSecret{
  143. Spec: esv1.ExternalSecretSpec{
  144. Target: esv1.ExternalSecretTarget{
  145. Manifest: &esv1.ManifestReference{
  146. APIVersion: "v1",
  147. Kind: "",
  148. },
  149. },
  150. },
  151. },
  152. allowGenericTargets: true,
  153. expectedError: true,
  154. errorContains: "kind is required",
  155. },
  156. }
  157. for _, tt := range tests {
  158. t.Run(tt.name, func(t *testing.T) {
  159. r := &Reconciler{
  160. AllowGenericTargets: tt.allowGenericTargets,
  161. }
  162. log := ctrl.Log.WithName("test")
  163. err := r.validateGenericTarget(log, tt.es)
  164. if tt.expectedError {
  165. assert.Error(t, err)
  166. if tt.errorContains != "" {
  167. assert.Contains(t, err.Error(), tt.errorContains)
  168. }
  169. } else {
  170. assert.NoError(t, err)
  171. }
  172. })
  173. }
  174. }
  175. func TestGetTargetGVK(t *testing.T) {
  176. tests := []struct {
  177. name string
  178. es *esv1.ExternalSecret
  179. expected schema.GroupVersionKind
  180. }{
  181. {
  182. name: "ConfigMap target",
  183. es: &esv1.ExternalSecret{
  184. Spec: esv1.ExternalSecretSpec{
  185. Target: esv1.ExternalSecretTarget{
  186. Manifest: &esv1.ManifestReference{
  187. APIVersion: "v1",
  188. Kind: "ConfigMap",
  189. },
  190. },
  191. },
  192. },
  193. expected: schema.GroupVersionKind{
  194. Group: "",
  195. Version: "v1",
  196. Kind: "ConfigMap",
  197. },
  198. },
  199. {
  200. name: "ArgoCD Application target",
  201. es: &esv1.ExternalSecret{
  202. Spec: esv1.ExternalSecretSpec{
  203. Target: esv1.ExternalSecretTarget{
  204. Manifest: &esv1.ManifestReference{
  205. APIVersion: "argoproj.io/v1alpha1",
  206. Kind: "Application",
  207. },
  208. },
  209. },
  210. },
  211. expected: schema.GroupVersionKind{
  212. Group: "argoproj.io",
  213. Version: "v1alpha1",
  214. Kind: "Application",
  215. },
  216. },
  217. }
  218. for _, tt := range tests {
  219. t.Run(tt.name, func(t *testing.T) {
  220. result := getTargetGVK(tt.es)
  221. assert.Equal(t, tt.expected, result)
  222. })
  223. }
  224. }
  225. func TestGetTargetName(t *testing.T) {
  226. tests := []struct {
  227. name string
  228. es *esv1.ExternalSecret
  229. expected string
  230. }{
  231. {
  232. name: "Use target name when specified",
  233. es: &esv1.ExternalSecret{
  234. ObjectMeta: metav1.ObjectMeta{
  235. Name: "my-external-secret",
  236. },
  237. Spec: esv1.ExternalSecretSpec{
  238. Target: esv1.ExternalSecretTarget{
  239. Name: "custom-target-name",
  240. },
  241. },
  242. },
  243. expected: "custom-target-name",
  244. },
  245. {
  246. name: "Use ExternalSecret name when target name not specified",
  247. es: &esv1.ExternalSecret{
  248. ObjectMeta: metav1.ObjectMeta{
  249. Name: "my-external-secret",
  250. },
  251. Spec: esv1.ExternalSecretSpec{
  252. Target: esv1.ExternalSecretTarget{
  253. Name: "",
  254. },
  255. },
  256. },
  257. expected: "my-external-secret",
  258. },
  259. }
  260. for _, tt := range tests {
  261. t.Run(tt.name, func(t *testing.T) {
  262. result := getTargetName(tt.es)
  263. assert.Equal(t, tt.expected, result)
  264. })
  265. }
  266. }
  267. func TestCreateSimpleManifest(t *testing.T) {
  268. tests := []struct {
  269. name string
  270. kind string
  271. dataMap map[string][]byte
  272. validate func(t *testing.T, obj *unstructured.Unstructured)
  273. }{
  274. {
  275. name: "ConfigMap with data",
  276. kind: "ConfigMap",
  277. dataMap: map[string][]byte{
  278. "key1": []byte("value1"),
  279. "key2": []byte("value2"),
  280. },
  281. validate: func(t *testing.T, obj *unstructured.Unstructured) {
  282. // Directly access the data field
  283. data, ok := obj.Object["data"].(map[string]string)
  284. require.True(t, ok, "data should be map[string]string")
  285. assert.Equal(t, "value1", data["key1"])
  286. assert.Equal(t, "value2", data["key2"])
  287. },
  288. },
  289. {
  290. name: "Custom resource with spec.data",
  291. kind: "CustomResource",
  292. dataMap: map[string][]byte{
  293. "config": []byte("my-config"),
  294. },
  295. validate: func(t *testing.T, obj *unstructured.Unstructured) {
  296. spec, ok := obj.Object["spec"].(map[string]any)
  297. require.True(t, ok, "spec should be map[string]interface{}")
  298. data, ok := spec["data"].(map[string]string)
  299. require.True(t, ok, "spec.data should be map[string]string")
  300. assert.Equal(t, "my-config", data["config"])
  301. },
  302. },
  303. }
  304. for _, tt := range tests {
  305. t.Run(tt.name, func(t *testing.T) {
  306. r := &Reconciler{}
  307. obj := &unstructured.Unstructured{
  308. Object: make(map[string]any),
  309. }
  310. obj.SetKind(tt.kind)
  311. result := r.createSimpleManifest(obj, tt.dataMap)
  312. assert.NotNil(t, result)
  313. if tt.validate != nil {
  314. tt.validate(t, result)
  315. }
  316. })
  317. }
  318. }
  319. func TestApplyTemplateToManifest_SimpleConfigMap(t *testing.T) {
  320. // Setup
  321. _ = esv1.AddToScheme(scheme.Scheme)
  322. fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme.Scheme).Build()
  323. r := &Reconciler{
  324. Client: fakeClient,
  325. Scheme: scheme.Scheme,
  326. }
  327. es := &esv1.ExternalSecret{
  328. ObjectMeta: metav1.ObjectMeta{
  329. Name: "test-es",
  330. Namespace: "default",
  331. },
  332. Spec: esv1.ExternalSecretSpec{
  333. Target: esv1.ExternalSecretTarget{
  334. Name: "test-configmap",
  335. Manifest: &esv1.ManifestReference{
  336. APIVersion: "v1",
  337. Kind: "ConfigMap",
  338. },
  339. },
  340. },
  341. }
  342. dataMap := map[string][]byte{
  343. "key1": []byte("value1"),
  344. "key2": []byte("value2"),
  345. }
  346. // Execute
  347. result, err := r.applyTemplateToManifest(context.Background(), es, dataMap, nil)
  348. // Verify
  349. require.NoError(t, err)
  350. assert.NotNil(t, result)
  351. assert.Equal(t, "ConfigMap", result.GetKind())
  352. assert.Equal(t, "test-configmap", result.GetName())
  353. assert.Equal(t, "default", result.GetNamespace())
  354. // Verify data
  355. data, ok := result.Object["data"].(map[string]string)
  356. require.True(t, ok, "data should be map[string]string")
  357. assert.Equal(t, "value1", data["key1"])
  358. assert.Equal(t, "value2", data["key2"])
  359. // Verify managed label
  360. labels := result.GetLabels()
  361. assert.Equal(t, esv1.LabelManagedValue, labels[esv1.LabelManaged])
  362. }
  363. func TestApplyTemplateToManifest_WithMetadata(t *testing.T) {
  364. // Setup
  365. _ = esv1.AddToScheme(scheme.Scheme)
  366. fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme.Scheme).Build()
  367. r := &Reconciler{
  368. Client: fakeClient,
  369. Scheme: scheme.Scheme,
  370. }
  371. es := &esv1.ExternalSecret{
  372. ObjectMeta: metav1.ObjectMeta{
  373. Name: "test-es",
  374. Namespace: "default",
  375. },
  376. Spec: esv1.ExternalSecretSpec{
  377. Target: esv1.ExternalSecretTarget{
  378. Name: "test-configmap",
  379. Manifest: &esv1.ManifestReference{
  380. APIVersion: "v1",
  381. Kind: "ConfigMap",
  382. },
  383. Template: &esv1.ExternalSecretTemplate{
  384. EngineVersion: esv1.TemplateEngineV2, // Set engine version
  385. Metadata: esv1.ExternalSecretTemplateMetadata{
  386. Labels: map[string]string{
  387. "app": "myapp",
  388. "tier": "backend",
  389. },
  390. Annotations: map[string]string{
  391. "description": "This is a test",
  392. },
  393. },
  394. },
  395. },
  396. },
  397. }
  398. dataMap := map[string][]byte{
  399. "config": []byte("test-config"),
  400. }
  401. // Execute
  402. result, err := r.applyTemplateToManifest(context.Background(), es, dataMap, nil)
  403. // Verify
  404. require.NoError(t, err)
  405. assert.NotNil(t, result)
  406. // Verify labels
  407. labels := result.GetLabels()
  408. assert.Equal(t, "myapp", labels["app"])
  409. assert.Equal(t, "backend", labels["tier"])
  410. assert.Equal(t, esv1.LabelManagedValue, labels[esv1.LabelManaged])
  411. // Verify annotations
  412. annotations := result.GetAnnotations()
  413. assert.Equal(t, "This is a test", annotations["description"])
  414. }
  415. func TestApplyTemplateToManifest_AppliesOwnershipWhenCreationPolicyOwner(t *testing.T) {
  416. _ = esv1.AddToScheme(scheme.Scheme)
  417. fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme.Scheme).Build()
  418. r := &Reconciler{Client: fakeClient, Scheme: scheme.Scheme}
  419. es := &esv1.ExternalSecret{
  420. ObjectMeta: metav1.ObjectMeta{
  421. Name: "test-es",
  422. Namespace: "default",
  423. UID: "abc-123",
  424. },
  425. Spec: esv1.ExternalSecretSpec{
  426. Target: esv1.ExternalSecretTarget{
  427. Name: "test-configmap",
  428. CreationPolicy: esv1.CreatePolicyOwner,
  429. Manifest: &esv1.ManifestReference{
  430. APIVersion: "v1",
  431. Kind: "ConfigMap",
  432. },
  433. },
  434. },
  435. }
  436. result, err := r.applyTemplateToManifest(context.Background(), es, map[string][]byte{"key": []byte("val")}, nil)
  437. require.NoError(t, err)
  438. owners := result.GetOwnerReferences()
  439. require.Len(t, owners, 1)
  440. assert.Equal(t, "test-es", owners[0].Name)
  441. assert.True(t, *owners[0].Controller)
  442. labels := result.GetLabels()
  443. assert.Equal(t, esutils.ObjectHash("default/test-es"), labels[esv1.LabelOwner])
  444. }
  445. func TestApplyOwnership(t *testing.T) {
  446. _ = esv1.AddToScheme(scheme.Scheme)
  447. isController := true
  448. tests := []struct {
  449. name string
  450. creationPolicy esv1.ExternalSecretCreationPolicy
  451. existing *unstructured.Unstructured
  452. expectedErr error
  453. validate func(t *testing.T, result *unstructured.Unstructured)
  454. }{
  455. {
  456. name: "removes LabelOwner when policy is not Owner",
  457. creationPolicy: esv1.CreatePolicyOrphan,
  458. existing: func() *unstructured.Unstructured {
  459. u := &unstructured.Unstructured{}
  460. u.SetLabels(map[string]string{
  461. esv1.LabelOwner: esutils.ObjectHash("default/test-es"),
  462. })
  463. return u
  464. }(),
  465. validate: func(t *testing.T, result *unstructured.Unstructured) {
  466. assert.Empty(t, result.GetLabels()[esv1.LabelOwner])
  467. },
  468. },
  469. {
  470. name: "removes owner reference when policy changes from Owner to Orphan",
  471. creationPolicy: esv1.CreatePolicyOrphan,
  472. existing: func() *unstructured.Unstructured {
  473. u := &unstructured.Unstructured{}
  474. u.SetGroupVersionKind(schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"})
  475. u.SetOwnerReferences([]metav1.OwnerReference{
  476. {
  477. APIVersion: esv1.SchemeGroupVersion.String(),
  478. Kind: esv1.ExtSecretKind,
  479. Name: "test-es",
  480. UID: "abc-123",
  481. Controller: &isController,
  482. },
  483. })
  484. return u
  485. }(),
  486. validate: func(t *testing.T, result *unstructured.Unstructured) {
  487. assert.Empty(t, result.GetOwnerReferences())
  488. },
  489. },
  490. {
  491. name: "returns ErrSecretIsOwned when owned by a different ExternalSecret",
  492. creationPolicy: esv1.CreatePolicyOwner,
  493. existing: func() *unstructured.Unstructured {
  494. u := &unstructured.Unstructured{}
  495. u.SetOwnerReferences([]metav1.OwnerReference{
  496. {
  497. APIVersion: esv1.SchemeGroupVersion.String(),
  498. Kind: esv1.ExtSecretKind,
  499. Name: "other-es",
  500. UID: "xyz-999",
  501. Controller: &isController,
  502. },
  503. })
  504. return u
  505. }(),
  506. expectedErr: ErrSecretIsOwned,
  507. },
  508. }
  509. for _, tt := range tests {
  510. t.Run(tt.name, func(t *testing.T) {
  511. fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme.Scheme).Build()
  512. r := &Reconciler{Client: fakeClient, Scheme: scheme.Scheme}
  513. es := &esv1.ExternalSecret{
  514. ObjectMeta: metav1.ObjectMeta{
  515. Name: "test-es",
  516. Namespace: "default",
  517. UID: "abc-123",
  518. },
  519. Spec: esv1.ExternalSecretSpec{
  520. Target: esv1.ExternalSecretTarget{
  521. Name: "test-configmap",
  522. CreationPolicy: tt.creationPolicy,
  523. Manifest: &esv1.ManifestReference{
  524. APIVersion: "v1",
  525. Kind: "ConfigMap",
  526. },
  527. },
  528. },
  529. }
  530. err := r.applyOwnership(es, tt.existing)
  531. if tt.expectedErr != nil {
  532. require.ErrorIs(t, err, tt.expectedErr)
  533. return
  534. }
  535. require.NoError(t, err)
  536. if tt.validate != nil {
  537. tt.validate(t, tt.existing)
  538. }
  539. })
  540. }
  541. }
  542. func TestApplyTemplateToManifest_NoOwnerRefWhenCreationPolicyOrphan(t *testing.T) {
  543. _ = esv1.AddToScheme(scheme.Scheme)
  544. fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme.Scheme).Build()
  545. r := &Reconciler{Client: fakeClient, Scheme: scheme.Scheme}
  546. es := &esv1.ExternalSecret{
  547. ObjectMeta: metav1.ObjectMeta{
  548. Name: "test-es",
  549. Namespace: "default",
  550. UID: "abc-123",
  551. },
  552. Spec: esv1.ExternalSecretSpec{
  553. Target: esv1.ExternalSecretTarget{
  554. Name: "test-configmap",
  555. CreationPolicy: esv1.CreatePolicyOrphan,
  556. Manifest: &esv1.ManifestReference{
  557. APIVersion: "v1",
  558. Kind: "ConfigMap",
  559. },
  560. },
  561. },
  562. }
  563. result, err := r.applyTemplateToManifest(context.Background(), es, map[string][]byte{"key": []byte("val")}, nil)
  564. require.NoError(t, err)
  565. assert.Empty(t, result.GetOwnerReferences())
  566. }
  567. func TestApplyTemplateToManifest_PropagatesESLabelsAndAnnotations(t *testing.T) {
  568. _ = esv1.AddToScheme(scheme.Scheme)
  569. fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme.Scheme).Build()
  570. r := &Reconciler{Client: fakeClient, Scheme: scheme.Scheme}
  571. es := &esv1.ExternalSecret{
  572. ObjectMeta: metav1.ObjectMeta{
  573. Name: "test-es",
  574. Namespace: "default",
  575. Labels: map[string]string{
  576. "app.kubernetes.io/instance": "my-argocd-app",
  577. "custom-label": "custom-value",
  578. },
  579. Annotations: map[string]string{
  580. "argocd.argoproj.io/tracking-id": "my-argocd-app:external-secrets.io/ExternalSecret:default/test-es",
  581. },
  582. },
  583. Spec: esv1.ExternalSecretSpec{
  584. Target: esv1.ExternalSecretTarget{
  585. Name: "test-configmap",
  586. Manifest: &esv1.ManifestReference{
  587. APIVersion: "v1",
  588. Kind: "ConfigMap",
  589. },
  590. },
  591. },
  592. }
  593. result, err := r.applyTemplateToManifest(context.Background(), es, map[string][]byte{"key": []byte("val")}, nil)
  594. require.NoError(t, err)
  595. labels := result.GetLabels()
  596. assert.Equal(t, "my-argocd-app", labels["app.kubernetes.io/instance"])
  597. assert.Equal(t, "custom-value", labels["custom-label"])
  598. assert.Equal(t, esv1.LabelManagedValue, labels[esv1.LabelManaged])
  599. annotations := result.GetAnnotations()
  600. assert.Equal(t, "my-argocd-app:external-secrets.io/ExternalSecret:default/test-es", annotations["argocd.argoproj.io/tracking-id"])
  601. }
  602. func TestApplyTemplateToManifest_TemplateMetadataWinsOverESLabels(t *testing.T) {
  603. _ = esv1.AddToScheme(scheme.Scheme)
  604. fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme.Scheme).Build()
  605. r := &Reconciler{Client: fakeClient, Scheme: scheme.Scheme}
  606. es := &esv1.ExternalSecret{
  607. ObjectMeta: metav1.ObjectMeta{
  608. Name: "test-es",
  609. Namespace: "default",
  610. Labels: map[string]string{
  611. "app.kubernetes.io/instance": "my-argocd-app",
  612. },
  613. },
  614. Spec: esv1.ExternalSecretSpec{
  615. Target: esv1.ExternalSecretTarget{
  616. Name: "test-configmap",
  617. Manifest: &esv1.ManifestReference{
  618. APIVersion: "v1",
  619. Kind: "ConfigMap",
  620. },
  621. Template: &esv1.ExternalSecretTemplate{
  622. EngineVersion: esv1.TemplateEngineV2,
  623. Metadata: esv1.ExternalSecretTemplateMetadata{
  624. Labels: map[string]string{
  625. "app": "explicit-template-label",
  626. },
  627. },
  628. },
  629. },
  630. },
  631. }
  632. result, err := r.applyTemplateToManifest(context.Background(), es, map[string][]byte{"key": []byte("val")}, nil)
  633. require.NoError(t, err)
  634. labels := result.GetLabels()
  635. assert.Equal(t, "explicit-template-label", labels["app"])
  636. assert.Empty(t, labels["app.kubernetes.io/instance"])
  637. assert.Equal(t, esv1.LabelManagedValue, labels[esv1.LabelManaged])
  638. }
  639. func TestApplyTemplateToManifest_NoESLabels(t *testing.T) {
  640. _ = esv1.AddToScheme(scheme.Scheme)
  641. fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme.Scheme).Build()
  642. r := &Reconciler{Client: fakeClient, Scheme: scheme.Scheme}
  643. es := &esv1.ExternalSecret{
  644. ObjectMeta: metav1.ObjectMeta{
  645. Name: "test-es",
  646. Namespace: "default",
  647. },
  648. Spec: esv1.ExternalSecretSpec{
  649. Target: esv1.ExternalSecretTarget{
  650. Name: "test-configmap",
  651. Manifest: &esv1.ManifestReference{
  652. APIVersion: "v1",
  653. Kind: "ConfigMap",
  654. },
  655. },
  656. },
  657. }
  658. result, err := r.applyTemplateToManifest(context.Background(), es, map[string][]byte{"key": []byte("val")}, nil)
  659. require.NoError(t, err)
  660. labels := result.GetLabels()
  661. assert.Equal(t, esv1.LabelManagedValue, labels[esv1.LabelManaged])
  662. assert.Len(t, labels, 1)
  663. }
  664. func TestGetGenericResource(t *testing.T) {
  665. // Setup
  666. _ = esv1.AddToScheme(scheme.Scheme)
  667. // Create a ConfigMap to find
  668. existingConfigMap := &unstructured.Unstructured{
  669. Object: map[string]any{
  670. "apiVersion": "v1",
  671. "kind": "ConfigMap",
  672. "metadata": map[string]any{
  673. "name": "test-cm",
  674. "namespace": "default",
  675. },
  676. "data": map[string]any{
  677. "key": "value",
  678. },
  679. },
  680. }
  681. _ = esv1.AddToScheme(scheme.Scheme)
  682. fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(existingConfigMap).Build()
  683. r := &Reconciler{
  684. Client: fakeClient,
  685. Scheme: scheme.Scheme,
  686. }
  687. es := &esv1.ExternalSecret{
  688. ObjectMeta: metav1.ObjectMeta{
  689. Name: "test-es",
  690. Namespace: "default",
  691. },
  692. Spec: esv1.ExternalSecretSpec{
  693. Target: esv1.ExternalSecretTarget{
  694. Name: "test-cm",
  695. Manifest: &esv1.ManifestReference{
  696. APIVersion: "v1",
  697. Kind: "ConfigMap",
  698. },
  699. },
  700. },
  701. }
  702. // Execute
  703. result, err := r.getGenericResource(context.Background(), logr.Discard(), es)
  704. // Verify
  705. require.NoError(t, err)
  706. assert.NotNil(t, result)
  707. assert.Equal(t, "ConfigMap", result.GetKind())
  708. assert.Equal(t, "test-cm", result.GetName())
  709. // Verify data
  710. data, found, err := unstructured.NestedStringMap(result.Object, "data")
  711. require.NoError(t, err)
  712. require.True(t, found)
  713. assert.Equal(t, "value", data["key"])
  714. }
  715. func TestGetGenericResource_NotFound(t *testing.T) {
  716. // Setup
  717. _ = esv1.AddToScheme(scheme.Scheme)
  718. fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme.Scheme).Build()
  719. r := &Reconciler{
  720. Client: fakeClient,
  721. Scheme: scheme.Scheme,
  722. }
  723. es := &esv1.ExternalSecret{
  724. ObjectMeta: metav1.ObjectMeta{
  725. Name: "test-es",
  726. Namespace: "default",
  727. },
  728. Spec: esv1.ExternalSecretSpec{
  729. Target: esv1.ExternalSecretTarget{
  730. Name: "nonexistent-cm",
  731. Manifest: &esv1.ManifestReference{
  732. APIVersion: "v1",
  733. Kind: "ConfigMap",
  734. },
  735. },
  736. },
  737. }
  738. // Execute
  739. result, err := r.getGenericResource(context.Background(), logr.Discard(), es)
  740. // Verify - should return an error and nil result when resource doesn't exist
  741. assert.Error(t, err)
  742. assert.True(t, apierrors.IsNotFound(err))
  743. assert.Nil(t, result)
  744. }
  745. func init() {
  746. // Initialize scheme for tests
  747. _ = esv1.AddToScheme(scheme.Scheme)
  748. _ = v1.AddToScheme(scheme.Scheme)
  749. }
  750. func TestApplyTemplateToManifest_LiteralWithDeployment(t *testing.T) {
  751. // Test that literal templates work with complex objects like Deployments
  752. _ = esv1.AddToScheme(scheme.Scheme)
  753. fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme.Scheme).Build()
  754. r := &Reconciler{
  755. Client: fakeClient,
  756. Scheme: scheme.Scheme,
  757. }
  758. es := &esv1.ExternalSecret{
  759. ObjectMeta: metav1.ObjectMeta{
  760. Name: "test-es",
  761. Namespace: "default",
  762. },
  763. Spec: esv1.ExternalSecretSpec{
  764. Target: esv1.ExternalSecretTarget{
  765. Name: "test-deployment",
  766. Manifest: &esv1.ManifestReference{
  767. APIVersion: "apps/v1",
  768. Kind: "Deployment",
  769. },
  770. Template: &esv1.ExternalSecretTemplate{
  771. EngineVersion: esv1.TemplateEngineV2,
  772. TemplateFrom: []esv1.TemplateFrom{
  773. {
  774. Target: "spec",
  775. Literal: new(`
  776. replicas: {{ .replicas }}
  777. selector:
  778. matchLabels:
  779. app: myapp
  780. template:
  781. metadata:
  782. labels:
  783. app: myapp
  784. spec:
  785. containers:
  786. - name: nginx
  787. image: nginx:{{ .version }}
  788. ports:
  789. - containerPort: 80
  790. `),
  791. },
  792. },
  793. },
  794. },
  795. },
  796. }
  797. dataMap := map[string][]byte{
  798. "replicas": []byte("3"),
  799. "version": []byte("1.21"),
  800. }
  801. result, err := r.applyTemplateToManifest(context.Background(), es, dataMap, nil)
  802. require.NoError(t, err)
  803. assert.NotNil(t, result)
  804. assert.Equal(t, "Deployment", result.GetKind())
  805. assert.Equal(t, "test-deployment", result.GetName())
  806. spec, found, err := unstructured.NestedMap(result.Object, "spec")
  807. require.NoError(t, err)
  808. require.True(t, found, "spec should exist")
  809. replicas, found, err := unstructured.NestedInt64(result.Object, "spec", "replicas")
  810. require.NoError(t, err)
  811. require.True(t, found, "spec.replicas should exist")
  812. assert.Equal(t, int64(3), replicas)
  813. containers, found, err := unstructured.NestedSlice(result.Object, "spec", "template", "spec", "containers")
  814. require.NoError(t, err)
  815. require.True(t, found, "containers should exist")
  816. require.Len(t, containers, 1, "should have 1 container")
  817. container, ok := containers[0].(map[string]any)
  818. require.True(t, ok, "container should be a map")
  819. assert.Equal(t, "nginx:1.21", container["image"])
  820. t.Logf("Result spec: %+v", spec)
  821. }
  822. func TestApplyTemplateToManifest_MergeBehavior(t *testing.T) {
  823. _ = esv1.AddToScheme(scheme.Scheme)
  824. fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme.Scheme).Build()
  825. r := &Reconciler{
  826. Client: fakeClient,
  827. Scheme: scheme.Scheme,
  828. }
  829. es := &esv1.ExternalSecret{
  830. ObjectMeta: metav1.ObjectMeta{
  831. Name: "test-es",
  832. Namespace: "default",
  833. },
  834. Spec: esv1.ExternalSecretSpec{
  835. Target: esv1.ExternalSecretTarget{
  836. Name: "test-slack-config",
  837. Manifest: &esv1.ManifestReference{
  838. APIVersion: "notification.toolkit.fluxcd.io/v1beta1",
  839. Kind: "Provider",
  840. },
  841. Template: &esv1.ExternalSecretTemplate{
  842. EngineVersion: esv1.TemplateEngineV2,
  843. TemplateFrom: []esv1.TemplateFrom{
  844. {
  845. Target: "spec.slack",
  846. Literal: new(`api_url: {{ .url }}`),
  847. },
  848. },
  849. },
  850. },
  851. },
  852. }
  853. existingResource := &unstructured.Unstructured{
  854. Object: map[string]any{
  855. "apiVersion": "notification.toolkit.fluxcd.io/v1beta1",
  856. "kind": "Provider",
  857. "metadata": map[string]any{
  858. "name": "test-slack-config",
  859. "namespace": "default",
  860. "resourceVersion": "12345",
  861. "uid": "test-uid-123",
  862. },
  863. "spec": map[string]any{
  864. "type": "slack",
  865. "slack": map[string]any{
  866. "channel": "general",
  867. "username": "bot",
  868. },
  869. },
  870. },
  871. }
  872. dataMap := map[string][]byte{
  873. "url": []byte("https://hooks.slack.com/services/XXX"),
  874. }
  875. result, err := r.applyTemplateToManifest(context.Background(), es, dataMap, existingResource)
  876. require.NoError(t, err)
  877. assert.NotNil(t, result)
  878. assert.Equal(t, "Provider", result.GetKind())
  879. assert.Equal(t, "test-slack-config", result.GetName())
  880. specType, found, err := unstructured.NestedString(result.Object, "spec", "type")
  881. require.NoError(t, err)
  882. require.True(t, found, "spec.type should be preserved")
  883. assert.Equal(t, "slack", specType, "spec.type should be preserved from existing resource")
  884. slackChannel, found, err := unstructured.NestedString(result.Object, "spec", "slack", "channel")
  885. require.NoError(t, err)
  886. require.True(t, found, "spec.slack.channel should be preserved")
  887. assert.Equal(t, "general", slackChannel, "spec.slack.channel should be preserved from existing resource")
  888. slackUsername, found, err := unstructured.NestedString(result.Object, "spec", "slack", "username")
  889. require.NoError(t, err)
  890. require.True(t, found, "spec.slack.username should be preserved")
  891. assert.Equal(t, "bot", slackUsername, "spec.slack.username should be preserved from existing resource")
  892. apiURL, found, err := unstructured.NestedString(result.Object, "spec", "slack", "api_url")
  893. require.NoError(t, err)
  894. require.True(t, found, "spec.slack.api_url should be added from template")
  895. assert.Equal(t, "https://hooks.slack.com/services/XXX", apiURL, "spec.slack.api_url should come from template")
  896. assert.Equal(t, "12345", result.GetResourceVersion(), "resourceVersion should be preserved")
  897. assert.Equal(t, "test-uid-123", string(result.GetUID()), "uid should be preserved")
  898. t.Logf("Result spec: %+v", result.Object["spec"])
  899. }
  900. func TestGenericTargetContentHash(t *testing.T) {
  901. tests := []struct {
  902. name string
  903. obj *unstructured.Unstructured
  904. wantErr bool
  905. }{
  906. {
  907. name: "hashes spec field",
  908. obj: &unstructured.Unstructured{
  909. Object: map[string]any{
  910. "spec": map[string]any{"key": "val"},
  911. },
  912. },
  913. },
  914. {
  915. name: "hashes data field when no spec",
  916. obj: &unstructured.Unstructured{
  917. Object: map[string]any{
  918. "data": map[string]any{"key": "val"},
  919. },
  920. },
  921. },
  922. {
  923. name: "prefers spec over data",
  924. obj: &unstructured.Unstructured{
  925. Object: map[string]any{
  926. "spec": map[string]any{"a": "1"},
  927. "data": map[string]any{"b": "2"},
  928. },
  929. },
  930. },
  931. {
  932. name: "errors when neither spec nor data",
  933. obj: &unstructured.Unstructured{
  934. Object: map[string]any{
  935. "status": map[string]any{"ready": true},
  936. },
  937. },
  938. wantErr: true,
  939. },
  940. }
  941. for _, tt := range tests {
  942. t.Run(tt.name, func(t *testing.T) {
  943. hash, err := genericTargetContentHash(tt.obj)
  944. if tt.wantErr {
  945. assert.Error(t, err)
  946. assert.Empty(t, hash)
  947. return
  948. }
  949. require.NoError(t, err)
  950. assert.NotEmpty(t, hash)
  951. })
  952. }
  953. t.Run("spec preferred over data produces spec hash", func(t *testing.T) {
  954. specData := map[string]any{"a": "1"}
  955. obj := &unstructured.Unstructured{
  956. Object: map[string]any{
  957. "spec": specData,
  958. "data": map[string]any{"b": "2"},
  959. },
  960. }
  961. hash, err := genericTargetContentHash(obj)
  962. require.NoError(t, err)
  963. assert.Equal(t, esutils.ObjectHash(specData), hash)
  964. })
  965. }
  966. func TestIsGenericTargetValid(t *testing.T) {
  967. makeES := func(policy esv1.ExternalSecretCreationPolicy) *esv1.ExternalSecret {
  968. return &esv1.ExternalSecret{
  969. Spec: esv1.ExternalSecretSpec{
  970. Target: esv1.ExternalSecretTarget{
  971. CreationPolicy: policy,
  972. },
  973. },
  974. }
  975. }
  976. makeTarget := func(uid string, labels map[string]string, annotations map[string]string, obj map[string]any) *unstructured.Unstructured {
  977. u := &unstructured.Unstructured{Object: obj}
  978. if uid != "" {
  979. u.SetUID(types.UID(uid))
  980. }
  981. u.SetLabels(labels)
  982. u.SetAnnotations(annotations)
  983. return u
  984. }
  985. t.Run("orphan policy always valid", func(t *testing.T) {
  986. valid, err := isGenericTargetValid(nil, makeES(esv1.CreatePolicyOrphan))
  987. require.NoError(t, err)
  988. assert.True(t, valid)
  989. })
  990. t.Run("nil target is invalid", func(t *testing.T) {
  991. valid, err := isGenericTargetValid(nil, makeES(esv1.CreatePolicyOwner))
  992. require.NoError(t, err)
  993. assert.False(t, valid)
  994. })
  995. t.Run("empty UID is invalid", func(t *testing.T) {
  996. obj := &unstructured.Unstructured{Object: map[string]any{}}
  997. valid, err := isGenericTargetValid(obj, makeES(esv1.CreatePolicyOwner))
  998. require.NoError(t, err)
  999. assert.False(t, valid)
  1000. })
  1001. t.Run("not managed is invalid", func(t *testing.T) {
  1002. obj := makeTarget("some-uid", map[string]string{}, nil, map[string]any{
  1003. "spec": map[string]any{"key": "val"},
  1004. })
  1005. valid, err := isGenericTargetValid(obj, makeES(esv1.CreatePolicyOwner))
  1006. require.NoError(t, err)
  1007. assert.False(t, valid)
  1008. })
  1009. t.Run("hash mismatch is invalid", func(t *testing.T) {
  1010. obj := makeTarget(
  1011. "some-uid",
  1012. map[string]string{esv1.LabelManaged: esv1.LabelManagedValue},
  1013. map[string]string{esv1.AnnotationDataHash: "wrong-hash"},
  1014. map[string]any{"spec": map[string]any{"key": "val"}},
  1015. )
  1016. valid, err := isGenericTargetValid(obj, makeES(esv1.CreatePolicyOwner))
  1017. require.NoError(t, err)
  1018. assert.False(t, valid)
  1019. })
  1020. t.Run("matching hash is valid", func(t *testing.T) {
  1021. specData := map[string]any{"key": "val"}
  1022. hash := esutils.ObjectHash(specData)
  1023. obj := makeTarget(
  1024. "some-uid",
  1025. map[string]string{esv1.LabelManaged: esv1.LabelManagedValue},
  1026. map[string]string{esv1.AnnotationDataHash: hash},
  1027. map[string]any{"spec": specData},
  1028. )
  1029. valid, err := isGenericTargetValid(obj, makeES(esv1.CreatePolicyOwner))
  1030. require.NoError(t, err)
  1031. assert.True(t, valid)
  1032. })
  1033. t.Run("errors when target has no spec or data", func(t *testing.T) {
  1034. obj := makeTarget(
  1035. "some-uid",
  1036. map[string]string{esv1.LabelManaged: esv1.LabelManagedValue},
  1037. nil,
  1038. map[string]any{"status": map[string]any{}},
  1039. )
  1040. _, err := isGenericTargetValid(obj, makeES(esv1.CreatePolicyOwner))
  1041. assert.Error(t, err)
  1042. })
  1043. }