externalsecret_controller_manifest_test.go 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873
  1. /*
  2. Copyright © 2025 ESO Maintainer Team
  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. "k8s.io/utils/ptr"
  28. ctrl "sigs.k8s.io/controller-runtime"
  29. fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
  30. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  31. "github.com/external-secrets/external-secrets/runtime/esutils"
  32. )
  33. func TestIsGenericTarget(t *testing.T) {
  34. tests := []struct {
  35. name string
  36. es *esv1.ExternalSecret
  37. expected bool
  38. }{
  39. {
  40. name: "nil manifest - Secret target",
  41. es: &esv1.ExternalSecret{
  42. Spec: esv1.ExternalSecretSpec{
  43. Target: esv1.ExternalSecretTarget{
  44. Manifest: nil,
  45. },
  46. },
  47. },
  48. expected: false,
  49. },
  50. {
  51. name: "ConfigMap manifest target",
  52. es: &esv1.ExternalSecret{
  53. Spec: esv1.ExternalSecretSpec{
  54. Target: esv1.ExternalSecretTarget{
  55. Manifest: &esv1.ManifestReference{
  56. APIVersion: "v1",
  57. Kind: "ConfigMap",
  58. },
  59. },
  60. },
  61. },
  62. expected: true,
  63. },
  64. {
  65. name: "Custom Resource manifest target",
  66. es: &esv1.ExternalSecret{
  67. Spec: esv1.ExternalSecretSpec{
  68. Target: esv1.ExternalSecretTarget{
  69. Manifest: &esv1.ManifestReference{
  70. APIVersion: "argoproj.io/v1alpha1",
  71. Kind: "Application",
  72. },
  73. },
  74. },
  75. },
  76. expected: true,
  77. },
  78. }
  79. for _, tt := range tests {
  80. t.Run(tt.name, func(t *testing.T) {
  81. result := isGenericTarget(tt.es)
  82. assert.Equal(t, tt.expected, result)
  83. })
  84. }
  85. }
  86. func TestValidateGenericTarget(t *testing.T) {
  87. tests := []struct {
  88. name string
  89. es *esv1.ExternalSecret
  90. allowGenericTargets bool
  91. expectedError bool
  92. errorContains string
  93. }{
  94. {
  95. name: "ConfigMap target - flag enabled - valid",
  96. es: &esv1.ExternalSecret{
  97. Spec: esv1.ExternalSecretSpec{
  98. Target: esv1.ExternalSecretTarget{
  99. Manifest: &esv1.ManifestReference{
  100. APIVersion: "v1",
  101. Kind: "ConfigMap",
  102. },
  103. },
  104. },
  105. },
  106. allowGenericTargets: true,
  107. expectedError: false,
  108. },
  109. {
  110. name: "ConfigMap target - flag disabled",
  111. es: &esv1.ExternalSecret{
  112. Spec: esv1.ExternalSecretSpec{
  113. Target: esv1.ExternalSecretTarget{
  114. Manifest: &esv1.ManifestReference{
  115. APIVersion: "v1",
  116. Kind: "ConfigMap",
  117. },
  118. },
  119. },
  120. },
  121. allowGenericTargets: false,
  122. expectedError: true,
  123. errorContains: "generic targets are disabled",
  124. },
  125. {
  126. name: "Missing APIVersion",
  127. es: &esv1.ExternalSecret{
  128. Spec: esv1.ExternalSecretSpec{
  129. Target: esv1.ExternalSecretTarget{
  130. Manifest: &esv1.ManifestReference{
  131. APIVersion: "",
  132. Kind: "ConfigMap",
  133. },
  134. },
  135. },
  136. },
  137. allowGenericTargets: true,
  138. expectedError: true,
  139. errorContains: "apiVersion is required",
  140. },
  141. {
  142. name: "Missing Kind",
  143. es: &esv1.ExternalSecret{
  144. Spec: esv1.ExternalSecretSpec{
  145. Target: esv1.ExternalSecretTarget{
  146. Manifest: &esv1.ManifestReference{
  147. APIVersion: "v1",
  148. Kind: "",
  149. },
  150. },
  151. },
  152. },
  153. allowGenericTargets: true,
  154. expectedError: true,
  155. errorContains: "kind is required",
  156. },
  157. }
  158. for _, tt := range tests {
  159. t.Run(tt.name, func(t *testing.T) {
  160. r := &Reconciler{
  161. AllowGenericTargets: tt.allowGenericTargets,
  162. }
  163. log := ctrl.Log.WithName("test")
  164. err := r.validateGenericTarget(log, tt.es)
  165. if tt.expectedError {
  166. assert.Error(t, err)
  167. if tt.errorContains != "" {
  168. assert.Contains(t, err.Error(), tt.errorContains)
  169. }
  170. } else {
  171. assert.NoError(t, err)
  172. }
  173. })
  174. }
  175. }
  176. func TestGetTargetGVK(t *testing.T) {
  177. tests := []struct {
  178. name string
  179. es *esv1.ExternalSecret
  180. expected schema.GroupVersionKind
  181. }{
  182. {
  183. name: "ConfigMap target",
  184. es: &esv1.ExternalSecret{
  185. Spec: esv1.ExternalSecretSpec{
  186. Target: esv1.ExternalSecretTarget{
  187. Manifest: &esv1.ManifestReference{
  188. APIVersion: "v1",
  189. Kind: "ConfigMap",
  190. },
  191. },
  192. },
  193. },
  194. expected: schema.GroupVersionKind{
  195. Group: "",
  196. Version: "v1",
  197. Kind: "ConfigMap",
  198. },
  199. },
  200. {
  201. name: "ArgoCD Application target",
  202. es: &esv1.ExternalSecret{
  203. Spec: esv1.ExternalSecretSpec{
  204. Target: esv1.ExternalSecretTarget{
  205. Manifest: &esv1.ManifestReference{
  206. APIVersion: "argoproj.io/v1alpha1",
  207. Kind: "Application",
  208. },
  209. },
  210. },
  211. },
  212. expected: schema.GroupVersionKind{
  213. Group: "argoproj.io",
  214. Version: "v1alpha1",
  215. Kind: "Application",
  216. },
  217. },
  218. }
  219. for _, tt := range tests {
  220. t.Run(tt.name, func(t *testing.T) {
  221. result := getTargetGVK(tt.es)
  222. assert.Equal(t, tt.expected, result)
  223. })
  224. }
  225. }
  226. func TestGetTargetName(t *testing.T) {
  227. tests := []struct {
  228. name string
  229. es *esv1.ExternalSecret
  230. expected string
  231. }{
  232. {
  233. name: "Use target name when specified",
  234. es: &esv1.ExternalSecret{
  235. ObjectMeta: metav1.ObjectMeta{
  236. Name: "my-external-secret",
  237. },
  238. Spec: esv1.ExternalSecretSpec{
  239. Target: esv1.ExternalSecretTarget{
  240. Name: "custom-target-name",
  241. },
  242. },
  243. },
  244. expected: "custom-target-name",
  245. },
  246. {
  247. name: "Use ExternalSecret name when target name not specified",
  248. es: &esv1.ExternalSecret{
  249. ObjectMeta: metav1.ObjectMeta{
  250. Name: "my-external-secret",
  251. },
  252. Spec: esv1.ExternalSecretSpec{
  253. Target: esv1.ExternalSecretTarget{
  254. Name: "",
  255. },
  256. },
  257. },
  258. expected: "my-external-secret",
  259. },
  260. }
  261. for _, tt := range tests {
  262. t.Run(tt.name, func(t *testing.T) {
  263. result := getTargetName(tt.es)
  264. assert.Equal(t, tt.expected, result)
  265. })
  266. }
  267. }
  268. func TestCreateSimpleManifest(t *testing.T) {
  269. tests := []struct {
  270. name string
  271. kind string
  272. dataMap map[string][]byte
  273. validate func(t *testing.T, obj *unstructured.Unstructured)
  274. }{
  275. {
  276. name: "ConfigMap with data",
  277. kind: "ConfigMap",
  278. dataMap: map[string][]byte{
  279. "key1": []byte("value1"),
  280. "key2": []byte("value2"),
  281. },
  282. validate: func(t *testing.T, obj *unstructured.Unstructured) {
  283. // Directly access the data field
  284. data, ok := obj.Object["data"].(map[string]string)
  285. require.True(t, ok, "data should be map[string]string")
  286. assert.Equal(t, "value1", data["key1"])
  287. assert.Equal(t, "value2", data["key2"])
  288. },
  289. },
  290. {
  291. name: "Custom resource with spec.data",
  292. kind: "CustomResource",
  293. dataMap: map[string][]byte{
  294. "config": []byte("my-config"),
  295. },
  296. validate: func(t *testing.T, obj *unstructured.Unstructured) {
  297. spec, ok := obj.Object["spec"].(map[string]interface{})
  298. require.True(t, ok, "spec should be map[string]interface{}")
  299. data, ok := spec["data"].(map[string]string)
  300. require.True(t, ok, "spec.data should be map[string]string")
  301. assert.Equal(t, "my-config", data["config"])
  302. },
  303. },
  304. }
  305. for _, tt := range tests {
  306. t.Run(tt.name, func(t *testing.T) {
  307. r := &Reconciler{}
  308. obj := &unstructured.Unstructured{
  309. Object: make(map[string]interface{}),
  310. }
  311. obj.SetKind(tt.kind)
  312. result := r.createSimpleManifest(obj, tt.dataMap)
  313. assert.NotNil(t, result)
  314. if tt.validate != nil {
  315. tt.validate(t, result)
  316. }
  317. })
  318. }
  319. }
  320. func TestApplyTemplateToManifest_SimpleConfigMap(t *testing.T) {
  321. // Setup
  322. _ = esv1.AddToScheme(scheme.Scheme)
  323. fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme.Scheme).Build()
  324. r := &Reconciler{
  325. Client: fakeClient,
  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. }
  370. es := &esv1.ExternalSecret{
  371. ObjectMeta: metav1.ObjectMeta{
  372. Name: "test-es",
  373. Namespace: "default",
  374. },
  375. Spec: esv1.ExternalSecretSpec{
  376. Target: esv1.ExternalSecretTarget{
  377. Name: "test-configmap",
  378. Manifest: &esv1.ManifestReference{
  379. APIVersion: "v1",
  380. Kind: "ConfigMap",
  381. },
  382. Template: &esv1.ExternalSecretTemplate{
  383. EngineVersion: esv1.TemplateEngineV2, // Set engine version
  384. Metadata: esv1.ExternalSecretTemplateMetadata{
  385. Labels: map[string]string{
  386. "app": "myapp",
  387. "tier": "backend",
  388. },
  389. Annotations: map[string]string{
  390. "description": "This is a test",
  391. },
  392. },
  393. },
  394. },
  395. },
  396. }
  397. dataMap := map[string][]byte{
  398. "config": []byte("test-config"),
  399. }
  400. // Execute
  401. result, err := r.applyTemplateToManifest(context.Background(), es, dataMap, nil)
  402. // Verify
  403. require.NoError(t, err)
  404. assert.NotNil(t, result)
  405. // Verify labels
  406. labels := result.GetLabels()
  407. assert.Equal(t, "myapp", labels["app"])
  408. assert.Equal(t, "backend", labels["tier"])
  409. assert.Equal(t, esv1.LabelManagedValue, labels[esv1.LabelManaged])
  410. // Verify annotations
  411. annotations := result.GetAnnotations()
  412. assert.Equal(t, "This is a test", annotations["description"])
  413. }
  414. func TestGetGenericResource(t *testing.T) {
  415. // Setup
  416. _ = esv1.AddToScheme(scheme.Scheme)
  417. // Create a ConfigMap to find
  418. existingConfigMap := &unstructured.Unstructured{
  419. Object: map[string]interface{}{
  420. "apiVersion": "v1",
  421. "kind": "ConfigMap",
  422. "metadata": map[string]interface{}{
  423. "name": "test-cm",
  424. "namespace": "default",
  425. },
  426. "data": map[string]interface{}{
  427. "key": "value",
  428. },
  429. },
  430. }
  431. _ = esv1.AddToScheme(scheme.Scheme)
  432. fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(existingConfigMap).Build()
  433. r := &Reconciler{
  434. Client: fakeClient,
  435. }
  436. es := &esv1.ExternalSecret{
  437. ObjectMeta: metav1.ObjectMeta{
  438. Name: "test-es",
  439. Namespace: "default",
  440. },
  441. Spec: esv1.ExternalSecretSpec{
  442. Target: esv1.ExternalSecretTarget{
  443. Name: "test-cm",
  444. Manifest: &esv1.ManifestReference{
  445. APIVersion: "v1",
  446. Kind: "ConfigMap",
  447. },
  448. },
  449. },
  450. }
  451. // Execute
  452. result, err := r.getGenericResource(context.Background(), logr.Discard(), es)
  453. // Verify
  454. require.NoError(t, err)
  455. assert.NotNil(t, result)
  456. assert.Equal(t, "ConfigMap", result.GetKind())
  457. assert.Equal(t, "test-cm", result.GetName())
  458. // Verify data
  459. data, found, err := unstructured.NestedStringMap(result.Object, "data")
  460. require.NoError(t, err)
  461. require.True(t, found)
  462. assert.Equal(t, "value", data["key"])
  463. }
  464. func TestGetGenericResource_NotFound(t *testing.T) {
  465. // Setup
  466. _ = esv1.AddToScheme(scheme.Scheme)
  467. fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme.Scheme).Build()
  468. r := &Reconciler{
  469. Client: fakeClient,
  470. }
  471. es := &esv1.ExternalSecret{
  472. ObjectMeta: metav1.ObjectMeta{
  473. Name: "test-es",
  474. Namespace: "default",
  475. },
  476. Spec: esv1.ExternalSecretSpec{
  477. Target: esv1.ExternalSecretTarget{
  478. Name: "nonexistent-cm",
  479. Manifest: &esv1.ManifestReference{
  480. APIVersion: "v1",
  481. Kind: "ConfigMap",
  482. },
  483. },
  484. },
  485. }
  486. // Execute
  487. result, err := r.getGenericResource(context.Background(), logr.Discard(), es)
  488. // Verify - should return an error and nil result when resource doesn't exist
  489. assert.Error(t, err)
  490. assert.True(t, apierrors.IsNotFound(err))
  491. assert.Nil(t, result)
  492. }
  493. func init() {
  494. // Initialize scheme for tests
  495. _ = esv1.AddToScheme(scheme.Scheme)
  496. _ = v1.AddToScheme(scheme.Scheme)
  497. }
  498. func TestApplyTemplateToManifest_LiteralWithDeployment(t *testing.T) {
  499. // Test that literal templates work with complex objects like Deployments
  500. _ = esv1.AddToScheme(scheme.Scheme)
  501. fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme.Scheme).Build()
  502. r := &Reconciler{
  503. Client: fakeClient,
  504. }
  505. es := &esv1.ExternalSecret{
  506. ObjectMeta: metav1.ObjectMeta{
  507. Name: "test-es",
  508. Namespace: "default",
  509. },
  510. Spec: esv1.ExternalSecretSpec{
  511. Target: esv1.ExternalSecretTarget{
  512. Name: "test-deployment",
  513. Manifest: &esv1.ManifestReference{
  514. APIVersion: "apps/v1",
  515. Kind: "Deployment",
  516. },
  517. Template: &esv1.ExternalSecretTemplate{
  518. EngineVersion: esv1.TemplateEngineV2,
  519. TemplateFrom: []esv1.TemplateFrom{
  520. {
  521. Target: "spec",
  522. Literal: ptr.To(`
  523. replicas: {{ .replicas }}
  524. selector:
  525. matchLabels:
  526. app: myapp
  527. template:
  528. metadata:
  529. labels:
  530. app: myapp
  531. spec:
  532. containers:
  533. - name: nginx
  534. image: nginx:{{ .version }}
  535. ports:
  536. - containerPort: 80
  537. `),
  538. },
  539. },
  540. },
  541. },
  542. },
  543. }
  544. dataMap := map[string][]byte{
  545. "replicas": []byte("3"),
  546. "version": []byte("1.21"),
  547. }
  548. result, err := r.applyTemplateToManifest(context.Background(), es, dataMap, nil)
  549. require.NoError(t, err)
  550. assert.NotNil(t, result)
  551. assert.Equal(t, "Deployment", result.GetKind())
  552. assert.Equal(t, "test-deployment", result.GetName())
  553. spec, found, err := unstructured.NestedMap(result.Object, "spec")
  554. require.NoError(t, err)
  555. require.True(t, found, "spec should exist")
  556. replicas, found, err := unstructured.NestedInt64(result.Object, "spec", "replicas")
  557. require.NoError(t, err)
  558. require.True(t, found, "spec.replicas should exist")
  559. assert.Equal(t, int64(3), replicas)
  560. containers, found, err := unstructured.NestedSlice(result.Object, "spec", "template", "spec", "containers")
  561. require.NoError(t, err)
  562. require.True(t, found, "containers should exist")
  563. require.Len(t, containers, 1, "should have 1 container")
  564. container, ok := containers[0].(map[string]any)
  565. require.True(t, ok, "container should be a map")
  566. assert.Equal(t, "nginx:1.21", container["image"])
  567. t.Logf("Result spec: %+v", spec)
  568. }
  569. func TestApplyTemplateToManifest_MergeBehavior(t *testing.T) {
  570. _ = esv1.AddToScheme(scheme.Scheme)
  571. fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme.Scheme).Build()
  572. r := &Reconciler{
  573. Client: fakeClient,
  574. }
  575. es := &esv1.ExternalSecret{
  576. ObjectMeta: metav1.ObjectMeta{
  577. Name: "test-es",
  578. Namespace: "default",
  579. },
  580. Spec: esv1.ExternalSecretSpec{
  581. Target: esv1.ExternalSecretTarget{
  582. Name: "test-slack-config",
  583. Manifest: &esv1.ManifestReference{
  584. APIVersion: "notification.toolkit.fluxcd.io/v1beta1",
  585. Kind: "Provider",
  586. },
  587. Template: &esv1.ExternalSecretTemplate{
  588. EngineVersion: esv1.TemplateEngineV2,
  589. TemplateFrom: []esv1.TemplateFrom{
  590. {
  591. Target: "spec.slack",
  592. Literal: ptr.To(`api_url: {{ .url }}`),
  593. },
  594. },
  595. },
  596. },
  597. },
  598. }
  599. existingResource := &unstructured.Unstructured{
  600. Object: map[string]interface{}{
  601. "apiVersion": "notification.toolkit.fluxcd.io/v1beta1",
  602. "kind": "Provider",
  603. "metadata": map[string]interface{}{
  604. "name": "test-slack-config",
  605. "namespace": "default",
  606. "resourceVersion": "12345",
  607. "uid": "test-uid-123",
  608. },
  609. "spec": map[string]interface{}{
  610. "type": "slack",
  611. "slack": map[string]interface{}{
  612. "channel": "general",
  613. "username": "bot",
  614. },
  615. },
  616. },
  617. }
  618. dataMap := map[string][]byte{
  619. "url": []byte("https://hooks.slack.com/services/XXX"),
  620. }
  621. result, err := r.applyTemplateToManifest(context.Background(), es, dataMap, existingResource)
  622. require.NoError(t, err)
  623. assert.NotNil(t, result)
  624. assert.Equal(t, "Provider", result.GetKind())
  625. assert.Equal(t, "test-slack-config", result.GetName())
  626. specType, found, err := unstructured.NestedString(result.Object, "spec", "type")
  627. require.NoError(t, err)
  628. require.True(t, found, "spec.type should be preserved")
  629. assert.Equal(t, "slack", specType, "spec.type should be preserved from existing resource")
  630. slackChannel, found, err := unstructured.NestedString(result.Object, "spec", "slack", "channel")
  631. require.NoError(t, err)
  632. require.True(t, found, "spec.slack.channel should be preserved")
  633. assert.Equal(t, "general", slackChannel, "spec.slack.channel should be preserved from existing resource")
  634. slackUsername, found, err := unstructured.NestedString(result.Object, "spec", "slack", "username")
  635. require.NoError(t, err)
  636. require.True(t, found, "spec.slack.username should be preserved")
  637. assert.Equal(t, "bot", slackUsername, "spec.slack.username should be preserved from existing resource")
  638. apiURL, found, err := unstructured.NestedString(result.Object, "spec", "slack", "api_url")
  639. require.NoError(t, err)
  640. require.True(t, found, "spec.slack.api_url should be added from template")
  641. assert.Equal(t, "https://hooks.slack.com/services/XXX", apiURL, "spec.slack.api_url should come from template")
  642. assert.Equal(t, "12345", result.GetResourceVersion(), "resourceVersion should be preserved")
  643. assert.Equal(t, "test-uid-123", string(result.GetUID()), "uid should be preserved")
  644. t.Logf("Result spec: %+v", result.Object["spec"])
  645. }
  646. func TestGenericTargetContentHash(t *testing.T) {
  647. tests := []struct {
  648. name string
  649. obj *unstructured.Unstructured
  650. wantErr bool
  651. }{
  652. {
  653. name: "hashes spec field",
  654. obj: &unstructured.Unstructured{
  655. Object: map[string]interface{}{
  656. "spec": map[string]interface{}{"key": "val"},
  657. },
  658. },
  659. },
  660. {
  661. name: "hashes data field when no spec",
  662. obj: &unstructured.Unstructured{
  663. Object: map[string]interface{}{
  664. "data": map[string]interface{}{"key": "val"},
  665. },
  666. },
  667. },
  668. {
  669. name: "prefers spec over data",
  670. obj: &unstructured.Unstructured{
  671. Object: map[string]interface{}{
  672. "spec": map[string]interface{}{"a": "1"},
  673. "data": map[string]interface{}{"b": "2"},
  674. },
  675. },
  676. },
  677. {
  678. name: "errors when neither spec nor data",
  679. obj: &unstructured.Unstructured{
  680. Object: map[string]interface{}{
  681. "status": map[string]interface{}{"ready": true},
  682. },
  683. },
  684. wantErr: true,
  685. },
  686. }
  687. for _, tt := range tests {
  688. t.Run(tt.name, func(t *testing.T) {
  689. hash, err := genericTargetContentHash(tt.obj)
  690. if tt.wantErr {
  691. assert.Error(t, err)
  692. assert.Empty(t, hash)
  693. return
  694. }
  695. require.NoError(t, err)
  696. assert.NotEmpty(t, hash)
  697. })
  698. }
  699. t.Run("spec preferred over data produces spec hash", func(t *testing.T) {
  700. specData := map[string]interface{}{"a": "1"}
  701. obj := &unstructured.Unstructured{
  702. Object: map[string]interface{}{
  703. "spec": specData,
  704. "data": map[string]interface{}{"b": "2"},
  705. },
  706. }
  707. hash, err := genericTargetContentHash(obj)
  708. require.NoError(t, err)
  709. assert.Equal(t, esutils.ObjectHash(specData), hash)
  710. })
  711. }
  712. func TestIsGenericTargetValid(t *testing.T) {
  713. makeES := func(policy esv1.ExternalSecretCreationPolicy) *esv1.ExternalSecret {
  714. return &esv1.ExternalSecret{
  715. Spec: esv1.ExternalSecretSpec{
  716. Target: esv1.ExternalSecretTarget{
  717. CreationPolicy: policy,
  718. },
  719. },
  720. }
  721. }
  722. makeTarget := func(uid string, labels map[string]string, annotations map[string]string, obj map[string]interface{}) *unstructured.Unstructured {
  723. u := &unstructured.Unstructured{Object: obj}
  724. if uid != "" {
  725. u.SetUID(types.UID(uid))
  726. }
  727. u.SetLabels(labels)
  728. u.SetAnnotations(annotations)
  729. return u
  730. }
  731. t.Run("orphan policy always valid", func(t *testing.T) {
  732. valid, err := isGenericTargetValid(nil, makeES(esv1.CreatePolicyOrphan))
  733. require.NoError(t, err)
  734. assert.True(t, valid)
  735. })
  736. t.Run("nil target is invalid", func(t *testing.T) {
  737. valid, err := isGenericTargetValid(nil, makeES(esv1.CreatePolicyOwner))
  738. require.NoError(t, err)
  739. assert.False(t, valid)
  740. })
  741. t.Run("empty UID is invalid", func(t *testing.T) {
  742. obj := &unstructured.Unstructured{Object: map[string]interface{}{}}
  743. valid, err := isGenericTargetValid(obj, makeES(esv1.CreatePolicyOwner))
  744. require.NoError(t, err)
  745. assert.False(t, valid)
  746. })
  747. t.Run("not managed is invalid", func(t *testing.T) {
  748. obj := makeTarget("some-uid", map[string]string{}, nil, map[string]interface{}{
  749. "spec": map[string]interface{}{"key": "val"},
  750. })
  751. valid, err := isGenericTargetValid(obj, makeES(esv1.CreatePolicyOwner))
  752. require.NoError(t, err)
  753. assert.False(t, valid)
  754. })
  755. t.Run("hash mismatch is invalid", func(t *testing.T) {
  756. obj := makeTarget(
  757. "some-uid",
  758. map[string]string{esv1.LabelManaged: esv1.LabelManagedValue},
  759. map[string]string{esv1.AnnotationDataHash: "wrong-hash"},
  760. map[string]interface{}{"spec": map[string]interface{}{"key": "val"}},
  761. )
  762. valid, err := isGenericTargetValid(obj, makeES(esv1.CreatePolicyOwner))
  763. require.NoError(t, err)
  764. assert.False(t, valid)
  765. })
  766. t.Run("matching hash is valid", func(t *testing.T) {
  767. specData := map[string]interface{}{"key": "val"}
  768. hash := esutils.ObjectHash(specData)
  769. obj := makeTarget(
  770. "some-uid",
  771. map[string]string{esv1.LabelManaged: esv1.LabelManagedValue},
  772. map[string]string{esv1.AnnotationDataHash: hash},
  773. map[string]interface{}{"spec": specData},
  774. )
  775. valid, err := isGenericTargetValid(obj, makeES(esv1.CreatePolicyOwner))
  776. require.NoError(t, err)
  777. assert.True(t, valid)
  778. })
  779. t.Run("errors when target has no spec or data", func(t *testing.T) {
  780. obj := makeTarget(
  781. "some-uid",
  782. map[string]string{esv1.LabelManaged: esv1.LabelManagedValue},
  783. nil,
  784. map[string]interface{}{"status": map[string]interface{}{}},
  785. )
  786. _, err := isGenericTargetValid(obj, makeES(esv1.CreatePolicyOwner))
  787. assert.Error(t, err)
  788. })
  789. }