Browse Source

feat: dynamic target implementation for external secrets sources (#5470)

* feat: dynamic target implementation for external secrets sources

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* added dynamic watching

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* simplify the usage

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* a bit of refactoring and changing error messages

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* fix: do not change target, use a new optional field instead

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* added extensive documentation

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* simplified applyToPath a bit

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* add missing generated files

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Gustavo Fernandes de Carvalho <17139678+gusfcarvalho@users.noreply.github.com>
Signed-off-by: Gergely Brautigam <skarlso777@gmail.com>

* added a an interface for the cache

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* further refined and tried to generalize the templating

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* added some tests for the new functionality

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* fix nilling out labels and annotations

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* fix check-diff

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* small fixes and comments

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* renamed and fixed the check-diff

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* work on switching away from the dynamic client

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* working on using the partial object cache instead

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* wording in the crd

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* fixing literal value parsing at path and secret values in none secret objects

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* pass in the context to the manager in test env

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* remove the incorrect if check in the indexer for secret targets

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* switch to informers

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* wire up the infomer to be actually useful

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* fix is managed check on ensure informer not allowing to register more tracking objects

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* added a generator test that can test the manifest usage easily

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* add resource rights and proper enabling of the feature flag

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* ward the informer and the deletion in case the feature flag is off

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* took care of the context todo and some refactoring

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

On-behalf-of: Gergely Brautigam <gergely.brautigam@sap.com>

* renamed non-Secret to Generic resource instead

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

On-behalf-of: Gergely Brautigam <gergely.brautigam@sap.com>

* extract the defer statement into the if instead of inside the reconcileGenericTarget

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

* adding comment abount status of the feature

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>

---------

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>
Signed-off-by: Gergely Brautigam <skarlso777@gmail.com>
Co-authored-by: Gustavo Fernandes de Carvalho <17139678+gusfcarvalho@users.noreply.github.com>
Gergely Brautigam 7 months ago
parent
commit
640d029c85
37 changed files with 2778 additions and 158 deletions
  1. 40 9
      apis/externalsecrets/v1/externalsecret_types.go
  2. 20 0
      apis/externalsecrets/v1/zz_generated.deepcopy.go
  3. 4 1
      cmd/controller/root.go
  4. 27 6
      config/crds/bases/external-secrets.io_clusterexternalsecrets.yaml
  5. 5 6
      config/crds/bases/external-secrets.io_clusterpushsecrets.yaml
  6. 26 6
      config/crds/bases/external-secrets.io_externalsecrets.yaml
  7. 5 6
      config/crds/bases/external-secrets.io_pushsecrets.yaml
  8. 3 0
      deploy/charts/external-secrets/README.md
  9. 3 0
      deploy/charts/external-secrets/templates/deployment.yaml
  10. 26 1
      deploy/charts/external-secrets/templates/rbac.yaml
  11. 11 0
      deploy/charts/external-secrets/values.schema.json
  12. 16 0
      deploy/charts/external-secrets/values.yaml
  13. 58 20
      deploy/crds/bundle.yaml
  14. 65 27
      docs/api/spec.md
  15. 1 0
      docs/guides/introduction.md
  16. 89 0
      docs/guides/targeting-custom-resources.md
  17. 43 0
      docs/snippets/manifest-advanced-path.yaml
  18. 19 0
      docs/snippets/manifest-argocd-app.yaml
  19. 23 0
      docs/snippets/manifest-basic-configmap.yaml
  20. 39 0
      docs/snippets/manifest-labeled-configmap.yaml
  21. 34 0
      docs/snippets/manifest-templated-configmap.yaml
  22. 33 0
      e2e/framework/addon/eso.go
  23. 135 0
      e2e/suites/generator/manifest.go
  24. 1 1
      e2e/suites/generator/suite_test.go
  25. 1 0
      hack/api-docs/mkdocs.yml
  26. 217 23
      pkg/controllers/externalsecret/externalsecret_controller.go
  27. 295 0
      pkg/controllers/externalsecret/externalsecret_controller_manifest.go
  28. 629 0
      pkg/controllers/externalsecret/externalsecret_controller_manifest_test.go
  29. 156 0
      pkg/controllers/externalsecret/externalsecret_controller_watch_test.go
  30. 310 0
      pkg/controllers/externalsecret/informer_manager.go
  31. 1 1
      pkg/controllers/externalsecret/suite_test.go
  32. 1 1
      pkg/controllers/templating/parser.go
  33. 2 2
      runtime/template/engine.go
  34. 196 24
      runtime/template/v2/template.go
  35. 238 24
      runtime/template/v2/template_test.go
  36. 3 0
      tests/__snapshot__/clusterexternalsecret-v1.yaml
  37. 3 0
      tests/__snapshot__/externalsecret-v1.yaml

+ 40 - 9
apis/externalsecrets/v1/externalsecret_types.go

@@ -141,9 +141,13 @@ type TemplateFrom struct {
 	ConfigMap *TemplateRef `json:"configMap,omitempty"`
 	Secret    *TemplateRef `json:"secret,omitempty"`
 
+	// Target specifies where to place the template result.
+	// For Secret resources, common values are: "Data", "Annotations", "Labels".
+	// For custom resources (when spec.target.manifest is set), this supports
+	// nested paths like "spec.database.config" or "data".
 	// +optional
 	// +kubebuilder:default="Data"
-	Target TemplateTarget `json:"target,omitempty"`
+	Target string `json:"target,omitempty"`
 
 	// +optional
 	Literal *string `json:"literal,omitempty"`
@@ -159,15 +163,11 @@ const (
 	TemplateScopeKeysAndValues TemplateScope = "KeysAndValues"
 )
 
-// TemplateTarget specifies where the rendered templates should be applied.
-// +kubebuilder:validation:Enum=Data;Annotations;Labels
-type TemplateTarget string
-
-// These are used to define the target of templates.
+// These constants are provided for convenience but Target accepts any string.
 const (
-	TemplateTargetData        TemplateTarget = "Data"
-	TemplateTargetAnnotations TemplateTarget = "Annotations"
-	TemplateTargetLabels      TemplateTarget = "Labels"
+	TemplateTargetData        = "Data"
+	TemplateTargetAnnotations = "Annotations"
+	TemplateTargetLabels      = "Labels"
 )
 
 // TemplateRef specifies a reference to either a ConfigMap or a Secret resource.
@@ -194,6 +194,21 @@ type TemplateRefItem struct {
 	TemplateAs TemplateScope `json:"templateAs,omitempty"`
 }
 
+// ManifestReference defines a custom Kubernetes resource type to be created
+// instead of a Secret. This allows ExternalSecret to create ConfigMaps,
+// Custom Resources, or any other Kubernetes resource type.
+type ManifestReference struct {
+	// APIVersion of the target resource (e.g., "v1" for ConfigMap, "argoproj.io/v1alpha1" for ArgoCD Application)
+	// +kubebuilder:validation:Required
+	// +kubebuilder:validation:MinLength:=1
+	APIVersion string `json:"apiVersion"`
+
+	// Kind of the target resource (e.g., "ConfigMap", "Application")
+	// +kubebuilder:validation:Required
+	// +kubebuilder:validation:MinLength:=1
+	Kind string `json:"kind"`
+}
+
 // ExternalSecretTarget defines the Kubernetes Secret to be created,
 // there can be only one target per ExternalSecret.
 type ExternalSecretTarget struct {
@@ -221,6 +236,13 @@ type ExternalSecretTarget struct {
 	// +optional
 	Template *ExternalSecretTemplate `json:"template,omitempty"`
 
+	// Manifest defines a custom Kubernetes resource to create instead of a Secret.
+	// When specified, ExternalSecret will create the resource type defined here
+	// (e.g., ConfigMap, Custom Resource) instead of a Secret.
+	// Warning: Using Generic target. Make sure access policies and encryption are properly configured.
+	// +optional
+	Manifest *ManifestReference `json:"manifest,omitempty"`
+
 	// Immutable defines if the final secret will be immutable
 	// +optional
 	Immutable bool `json:"immutable,omitempty"`
@@ -607,6 +629,15 @@ const (
 	ReasonDeleted = "Deleted"
 	// ReasonMissingProviderSecret indicates that the provider secret is missing.
 	ReasonMissingProviderSecret = "MissingProviderSecret"
+
+	// ConditionReasonResourceSynced indicates that the secrets was synced.
+	ConditionReasonResourceSynced = "ResourceSynced"
+	// ConditionReasonResourceSyncedError indicates that there was an error syncing the secret.
+	ConditionReasonResourceSyncedError = "ResourceSyncedError"
+	// ConditionReasonResourceDeleted indicates that the secret has been deleted.
+	ConditionReasonResourceDeleted = "ResourceDeleted"
+	// ConditionReasonResourceMissing indicates that the secret is missing.
+	ConditionReasonResourceMissing = "ResourceMissing"
 )
 
 // ExternalSecretStatus defines the observed state of ExternalSecret.

+ 20 - 0
apis/externalsecrets/v1/zz_generated.deepcopy.go

@@ -1689,6 +1689,11 @@ func (in *ExternalSecretTarget) DeepCopyInto(out *ExternalSecretTarget) {
 		*out = new(ExternalSecretTemplate)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.Manifest != nil {
+		in, out := &in.Manifest, &out.Manifest
+		*out = new(ManifestReference)
+		**out = **in
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalSecretTarget.
@@ -2504,6 +2509,21 @@ func (in *MachineIdentityScopeInWorkspace) DeepCopy() *MachineIdentityScopeInWor
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ManifestReference) DeepCopyInto(out *ManifestReference) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManifestReference.
+func (in *ManifestReference) DeepCopy() *ManifestReference {
+	if in == nil {
+		return nil
+	}
+	out := new(ManifestReference)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *NTLMProtocol) DeepCopyInto(out *NTLMProtocol) {
 	*out = *in

+ 4 - 1
cmd/controller/root.go

@@ -100,6 +100,7 @@ var (
 	tlsCiphers                            string
 	tlsMinVersion                         string
 	enableHTTP2                           bool
+	allowGenericTargets                   bool
 )
 
 const (
@@ -251,7 +252,8 @@ var rootCmd = &cobra.Command{
 			ClusterSecretStoreEnabled: enableClusterStoreReconciler,
 			EnableFloodGate:           enableFloodGate,
 			EnableGeneratorState:      enableGeneratorState,
-		}).SetupWithManager(mgr, controller.Options{
+			AllowGenericTargets:       allowGenericTargets,
+		}).SetupWithManager(cmd.Context(), mgr, controller.Options{
 			MaxConcurrentReconciles: concurrent,
 			RateLimiter:             ctrlcommon.BuildRateLimiter(),
 		}); err != nil {
@@ -364,6 +366,7 @@ func init() {
 	rootCmd.Flags().BoolVar(&enableExtendedMetricLabels, "enable-extended-metric-labels", false, "Enable recommended kubernetes annotations as labels in metrics.")
 	rootCmd.Flags().BoolVar(&enableHTTP2, "enable-http2", false,
 		"If set, HTTP/2 will be enabled for the metrics server")
+	rootCmd.Flags().BoolVar(&allowGenericTargets, "unsafe-allow-generic-targets", false, "Enable support for creating generic resources (ConfigMaps, Custom Resources). WARNING: Using generic resources, please sure all policies are correctly configured.")
 	fs := feature.Features()
 	for _, f := range fs {
 		rootCmd.Flags().AddFlagSet(f.Flags)

+ 27 - 6
config/crds/bases/external-secrets.io_clusterexternalsecrets.yaml

@@ -531,6 +531,28 @@ spec:
                         description: Immutable defines if the final secret will be
                           immutable
                         type: boolean
+                      manifest:
+                        description: |-
+                          Manifest defines a custom Kubernetes resource to create instead of a Secret.
+                          When specified, ExternalSecret will create the resource type defined here
+                          (e.g., ConfigMap, Custom Resource) instead of a Secret.
+                          Warning: Using Generic target. Make sure access policies and encryption are properly configured.
+                        properties:
+                          apiVersion:
+                            description: APIVersion of the target resource (e.g.,
+                              "v1" for ConfigMap, "argoproj.io/v1alpha1" for ArgoCD
+                              Application)
+                            minLength: 1
+                            type: string
+                          kind:
+                            description: Kind of the target resource (e.g., "ConfigMap",
+                              "Application")
+                            minLength: 1
+                            type: string
+                        required:
+                        - apiVersion
+                        - kind
+                        type: object
                       name:
                         description: |-
                           The name of the Secret resource to be managed.
@@ -673,12 +695,11 @@ spec:
                                   type: object
                                 target:
                                   default: Data
-                                  description: TemplateTarget specifies where the
-                                    rendered templates should be applied.
-                                  enum:
-                                  - Data
-                                  - Annotations
-                                  - Labels
+                                  description: |-
+                                    Target specifies where to place the template result.
+                                    For Secret resources, common values are: "Data", "Annotations", "Labels".
+                                    For custom resources (when spec.target.manifest is set), this supports
+                                    nested paths like "spec.database.config" or "data".
                                   type: string
                               type: object
                             type: array

+ 5 - 6
config/crds/bases/external-secrets.io_clusterpushsecrets.yaml

@@ -488,12 +488,11 @@ spec:
                               type: object
                             target:
                               default: Data
-                              description: TemplateTarget specifies where the rendered
-                                templates should be applied.
-                              enum:
-                              - Data
-                              - Annotations
-                              - Labels
+                              description: |-
+                                Target specifies where to place the template result.
+                                For Secret resources, common values are: "Data", "Annotations", "Labels".
+                                For custom resources (when spec.target.manifest is set), this supports
+                                nested paths like "spec.database.config" or "data".
                               type: string
                           type: object
                         type: array

+ 26 - 6
config/crds/bases/external-secrets.io_externalsecrets.yaml

@@ -510,6 +510,27 @@ spec:
                   immutable:
                     description: Immutable defines if the final secret will be immutable
                     type: boolean
+                  manifest:
+                    description: |-
+                      Manifest defines a custom Kubernetes resource to create instead of a Secret.
+                      When specified, ExternalSecret will create the resource type defined here
+                      (e.g., ConfigMap, Custom Resource) instead of a Secret.
+                      Warning: Using Generic target. Make sure access policies and encryption are properly configured.
+                    properties:
+                      apiVersion:
+                        description: APIVersion of the target resource (e.g., "v1"
+                          for ConfigMap, "argoproj.io/v1alpha1" for ArgoCD Application)
+                        minLength: 1
+                        type: string
+                      kind:
+                        description: Kind of the target resource (e.g., "ConfigMap",
+                          "Application")
+                        minLength: 1
+                        type: string
+                    required:
+                    - apiVersion
+                    - kind
+                    type: object
                   name:
                     description: |-
                       The name of the Secret resource to be managed.
@@ -650,12 +671,11 @@ spec:
                               type: object
                             target:
                               default: Data
-                              description: TemplateTarget specifies where the rendered
-                                templates should be applied.
-                              enum:
-                              - Data
-                              - Annotations
-                              - Labels
+                              description: |-
+                                Target specifies where to place the template result.
+                                For Secret resources, common values are: "Data", "Annotations", "Labels".
+                                For custom resources (when spec.target.manifest is set), this supports
+                                nested paths like "spec.database.config" or "data".
                               type: string
                           type: object
                         type: array

+ 5 - 6
config/crds/bases/external-secrets.io_pushsecrets.yaml

@@ -408,12 +408,11 @@ spec:
                           type: object
                         target:
                           default: Data
-                          description: TemplateTarget specifies where the rendered
-                            templates should be applied.
-                          enum:
-                          - Data
-                          - Annotations
-                          - Labels
+                          description: |-
+                            Target specifies where to place the template result.
+                            For Secret resources, common values are: "Data", "Annotations", "Labels".
+                            For custom resources (when spec.target.manifest is set), this supports
+                            nested paths like "spec.database.config" or "data".
                           type: string
                       type: object
                     type: array

+ 3 - 0
deploy/charts/external-secrets/README.md

@@ -111,6 +111,9 @@ The command removes all the Kubernetes components associated with the chart and
 | extraVolumeMounts | list | `[]` |  |
 | extraVolumes | list | `[]` |  |
 | fullnameOverride | string | `""` |  |
+| genericTargets | object | `{"enabled":false,"resources":[]}` | Enable support for generic targets (ConfigMaps, Custom Resources). Warning: Using generic target. Make sure access policies and encryption are properly configured. When enabled, this grants the controller permissions to create/update/delete ConfigMaps and optionally other resource types specified in generic.resources. |
+| genericTargets.enabled | bool | `false` | Enable generic target support |
+| genericTargets.resources | list | `[]` | List of additional resource types to grant permissions for. Each entry should specify apiGroup, resources, and verbs. Example: resources:   - apiGroup: "argoproj.io"     resources: ["applications"]     verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] |
 | global.affinity | object | `{}` |  |
 | global.compatibility.openshift.adaptSecurityContext | string | `"auto"` | Manages the securityContext properties to make them compatible with OpenShift. Possible values: auto - Apply configurations if it is detected that OpenShift is the target platform. force - Always apply configurations. disabled - No modification applied. |
 | global.nodeSelector | object | `{}` |  |

+ 3 - 0
deploy/charts/external-secrets/templates/deployment.yaml

@@ -94,6 +94,9 @@ spec:
           {{- if .Values.concurrent }}
           - --concurrent={{ .Values.concurrent }}
           {{- end }}
+          {{- if .Values.genericTargets.enabled }}
+          - --unsafe-allow-generic-targets=true
+          {{- end }}
           {{- range $key, $value := .Values.extraArgs }}
             {{- if $value }}
           - --{{ $key }}={{ $value }}

+ 26 - 1
deploy/charts/external-secrets/templates/rbac.yaml

@@ -155,6 +155,31 @@ rules:
     - "update"
     - "delete"
     - "patch"
+  {{- if .Values.genericTargets.enabled }}
+  # Generic target permissions (ConfigMaps)
+  - apiGroups:
+    - ""
+    resources:
+    - "configmaps"
+    verbs:
+    - "create"
+    - "update"
+    - "delete"
+    - "patch"
+  {{- range .Values.genericTargets.resources }}
+  # Custom resource permissions for non-Secret targets
+  - apiGroups:
+    - {{ .apiGroup | quote }}
+    resources:
+    {{- range .resources }}
+    - {{ . | quote }}
+    {{- end }}
+    verbs:
+    {{- range .verbs }}
+    - {{ . | quote }}
+    {{- end }}
+  {{- end }}
+  {{- end }}
   - apiGroups:
     - ""
     resources:
@@ -432,4 +457,4 @@ subjects:
   - kind: ServiceAccount
     name: {{ include "external-secrets.serviceAccountName" . }}
     namespace: {{ template "external-secrets.namespace" . }}
-{{- end }}
+{{- end }}

+ 11 - 0
deploy/charts/external-secrets/values.schema.json

@@ -338,6 +338,17 @@
         "fullnameOverride": {
             "type": "string"
         },
+        "genericTargets": {
+            "type": "object",
+            "properties": {
+                "enabled": {
+                    "type": "boolean"
+                },
+                "resources": {
+                    "type": "array"
+                }
+            }
+        },
         "global": {
             "type": "object",
             "properties": {

+ 16 - 0
deploy/charts/external-secrets/values.yaml

@@ -102,6 +102,22 @@ processClusterGenerator: true
 # -- if true, the operator will process push secret. Else, it will ignore them.
 processPushSecret: true
 
+# -- Enable support for generic targets (ConfigMaps, Custom Resources).
+# Warning: Using generic target. Make sure access policies and encryption are properly configured.
+# When enabled, this grants the controller permissions to create/update/delete
+# ConfigMaps and optionally other resource types specified in generic.resources.
+genericTargets:
+  # -- Enable generic target support
+  enabled: false
+  # -- List of additional resource types to grant permissions for.
+  # Each entry should specify apiGroup, resources, and verbs.
+  # Example:
+  # resources:
+  #   - apiGroup: "argoproj.io"
+  #     resources: ["applications"]
+  #     verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
+  resources: []
+
 # -- Specifies whether an external secret operator deployment be created.
 createOperator: true
 

+ 58 - 20
deploy/crds/bundle.yaml

@@ -503,6 +503,25 @@ spec:
                         immutable:
                           description: Immutable defines if the final secret will be immutable
                           type: boolean
+                        manifest:
+                          description: |-
+                            Manifest defines a custom Kubernetes resource to create instead of a Secret.
+                            When specified, ExternalSecret will create the resource type defined here
+                            (e.g., ConfigMap, Custom Resource) instead of a Secret.
+                            Warning: Using Generic target. Make sure access policies and encryption are properly configured.
+                          properties:
+                            apiVersion:
+                              description: APIVersion of the target resource (e.g., "v1" for ConfigMap, "argoproj.io/v1alpha1" for ArgoCD Application)
+                              minLength: 1
+                              type: string
+                            kind:
+                              description: Kind of the target resource (e.g., "ConfigMap", "Application")
+                              minLength: 1
+                              type: string
+                          required:
+                            - apiVersion
+                            - kind
+                          type: object
                         name:
                           description: |-
                             The name of the Secret resource to be managed.
@@ -630,11 +649,11 @@ spec:
                                     type: object
                                   target:
                                     default: Data
-                                    description: TemplateTarget specifies where the rendered templates should be applied.
-                                    enum:
-                                      - Data
-                                      - Annotations
-                                      - Labels
+                                    description: |-
+                                      Target specifies where to place the template result.
+                                      For Secret resources, common values are: "Data", "Annotations", "Labels".
+                                      For custom resources (when spec.target.manifest is set), this supports
+                                      nested paths like "spec.database.config" or "data".
                                     type: string
                                 type: object
                               type: array
@@ -1995,11 +2014,11 @@ spec:
                                 type: object
                               target:
                                 default: Data
-                                description: TemplateTarget specifies where the rendered templates should be applied.
-                                enum:
-                                  - Data
-                                  - Annotations
-                                  - Labels
+                                description: |-
+                                  Target specifies where to place the template result.
+                                  For Secret resources, common values are: "Data", "Annotations", "Labels".
+                                  For custom resources (when spec.target.manifest is set), this supports
+                                  nested paths like "spec.database.config" or "data".
                                 type: string
                             type: object
                           type: array
@@ -12059,6 +12078,25 @@ spec:
                     immutable:
                       description: Immutable defines if the final secret will be immutable
                       type: boolean
+                    manifest:
+                      description: |-
+                        Manifest defines a custom Kubernetes resource to create instead of a Secret.
+                        When specified, ExternalSecret will create the resource type defined here
+                        (e.g., ConfigMap, Custom Resource) instead of a Secret.
+                        Warning: Using Generic target. Make sure access policies and encryption are properly configured.
+                      properties:
+                        apiVersion:
+                          description: APIVersion of the target resource (e.g., "v1" for ConfigMap, "argoproj.io/v1alpha1" for ArgoCD Application)
+                          minLength: 1
+                          type: string
+                        kind:
+                          description: Kind of the target resource (e.g., "ConfigMap", "Application")
+                          minLength: 1
+                          type: string
+                      required:
+                        - apiVersion
+                        - kind
+                      type: object
                     name:
                       description: |-
                         The name of the Secret resource to be managed.
@@ -12186,11 +12224,11 @@ spec:
                                 type: object
                               target:
                                 default: Data
-                                description: TemplateTarget specifies where the rendered templates should be applied.
-                                enum:
-                                  - Data
-                                  - Annotations
-                                  - Labels
+                                description: |-
+                                  Target specifies where to place the template result.
+                                  For Secret resources, common values are: "Data", "Annotations", "Labels".
+                                  For custom resources (when spec.target.manifest is set), this supports
+                                  nested paths like "spec.database.config" or "data".
                                 type: string
                             type: object
                           type: array
@@ -13261,11 +13299,11 @@ spec:
                             type: object
                           target:
                             default: Data
-                            description: TemplateTarget specifies where the rendered templates should be applied.
-                            enum:
-                              - Data
-                              - Annotations
-                              - Labels
+                            description: |-
+                              Target specifies where to place the template result.
+                              For Secret resources, common values are: "Data", "Annotations", "Labels".
+                              For custom resources (when spec.target.manifest is set), this supports
+                              nested paths like "spec.database.config" or "data".
                             type: string
                         type: object
                       type: array

+ 65 - 27
docs/api/spec.md

@@ -4784,6 +4784,23 @@ ExternalSecretTemplate
 </tr>
 <tr>
 <td>
+<code>manifest</code></br>
+<em>
+<a href="#external-secrets.io/v1.ManifestReference">
+ManifestReference
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+<p>Manifest defines a custom Kubernetes resource to create instead of a Secret.
+When specified, ExternalSecret will create the resource type defined here
+(e.g., ConfigMap, Custom Resource) instead of a Secret.
+Warning: Using Generic target. Make sure access policies and encryption are properly configured.</p>
+</td>
+</tr>
+<tr>
+<td>
 <code>immutable</code></br>
 <em>
 bool
@@ -6858,6 +6875,49 @@ bool
 <td></td>
 </tr></tbody>
 </table>
+<h3 id="external-secrets.io/v1.ManifestReference">ManifestReference
+</h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1.ExternalSecretTarget">ExternalSecretTarget</a>)
+</p>
+<p>
+<p>ManifestReference defines a custom Kubernetes resource type to be created
+instead of a Secret. This allows ExternalSecret to create ConfigMaps,
+Custom Resources, or any other Kubernetes resource type.</p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Field</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td>
+<code>apiVersion</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>APIVersion of the target resource (e.g., &ldquo;v1&rdquo; for ConfigMap, &ldquo;argoproj.io/v1alpha1&rdquo; for ArgoCD Application)</p>
+</td>
+</tr>
+<tr>
+<td>
+<code>kind</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<p>Kind of the target resource (e.g., &ldquo;ConfigMap&rdquo;, &ldquo;Application&rdquo;)</p>
+</td>
+</tr>
+</tbody>
+</table>
 <h3 id="external-secrets.io/v1.NTLMProtocol">NTLMProtocol
 </h3>
 <p>
@@ -9880,13 +9940,15 @@ TemplateRef
 <td>
 <code>target</code></br>
 <em>
-<a href="#external-secrets.io/v1.TemplateTarget">
-TemplateTarget
-</a>
+string
 </em>
 </td>
 <td>
 <em>(Optional)</em>
+<p>Target specifies where to place the template result.
+For Secret resources, common values are: &ldquo;Data&rdquo;, &ldquo;Annotations&rdquo;, &ldquo;Labels&rdquo;.
+For custom resources (when spec.target.manifest is set), this supports
+nested paths like &ldquo;spec.database.config&rdquo; or &ldquo;data&rdquo;.</p>
 </td>
 </tr>
 <tr>
@@ -10031,30 +10093,6 @@ TemplateScope
 <td></td>
 </tr></tbody>
 </table>
-<h3 id="external-secrets.io/v1.TemplateTarget">TemplateTarget
-(<code>string</code> alias)</p></h3>
-<p>
-(<em>Appears on:</em>
-<a href="#external-secrets.io/v1.TemplateFrom">TemplateFrom</a>)
-</p>
-<p>
-<p>TemplateTarget specifies where the rendered templates should be applied.</p>
-</p>
-<table>
-<thead>
-<tr>
-<th>Value</th>
-<th>Description</th>
-</tr>
-</thead>
-<tbody><tr><td><p>&#34;Annotations&#34;</p></td>
-<td></td>
-</tr><tr><td><p>&#34;Data&#34;</p></td>
-<td></td>
-</tr><tr><td><p>&#34;Labels&#34;</p></td>
-<td></td>
-</tr></tbody>
-</table>
 <h3 id="external-secrets.io/v1.TokenAuth">TokenAuth
 </h3>
 <p>

+ 1 - 0
docs/guides/introduction.md

@@ -6,6 +6,7 @@ the API. Please pick one of the following guides:
 * [Multi-Tenancy Design Considerations](multi-tenancy.md)
 * [Find multiple secrets & Extract Secret values](getallsecrets.md)
 * [Advanced Templating](templating.md)
+* [Targeting Custom Resources](targeting-custom-resources.md)
 * [Generating Passwords using generators](generator.md)
 * [Ownership and Deletion Policy](ownership-deletion-policy.md)
 * [Key Rewriting](datafrom-rewrite.md)

+ 89 - 0
docs/guides/targeting-custom-resources.md

@@ -0,0 +1,89 @@
+# Targeting Custom Resources
+
+!!! warning "Maturity"
+    At the time of this writing (1.11.2025) this feature is in heavy alpha status. Please consider the following documentation with the limitations and guardrails
+    described below.
+
+External Secrets Operator can create and manage resources beyond Kubernetes Secrets. When you need to populate ConfigMaps or Custom Resource Definitions with secret data from your external provider, you can use the manifest target feature.
+
+!!! warning "Security Consideration"
+    Custom resources are not encrypted at rest by Kubernetes. Only use this feature when you need to populate resources that do not contain sensitive credentials, or when the target resource is encrypted by other means.
+
+This feature must be explicitly enabled in your deployment using the `--unsafe-allow-non-secret-targets` flag.
+
+!!! note "Namespaced Resources Only"
+    With this feature you can only target namespaced resources - and resources can only be managed by an ExternalSecret in the same namespace as the resource.
+
+!!! note "Performance"
+    Using generic targets or custom resources at the moment of this writing is ~20% slower than handling secrets due to certain missing features yet to be implemented.
+    We recommend not overusing this feature without too many objects until further performance improvement are implemented.
+
+## Basic ConfigMap Example
+
+The simplest use case is creating a ConfigMap from external secrets. This is useful when applications expect configuration in ConfigMaps rather than Secrets, or when the data is not sensitive.
+
+```yaml
+{% include 'manifest-basic-configmap.yaml' %}
+```
+
+This creates a ConfigMap named `app-config` with the data populated from your secret provider.
+
+## Custom Resource Definitions
+
+You can target any custom resource that exists in your cluster. This example creates an Argo CD Application resource:
+
+```yaml
+{% include 'manifest-argocd-app.yaml' %}
+```
+
+The operator will create or update the Application resource with the data from your external secret provider.
+
+## Templating with Custom Resources
+
+Templates work with custom resources just as they do with Secrets. You can use the `template.data` field to create structured configuration:
+
+```yaml
+{% include 'manifest-templated-configmap.yaml' %}
+```
+
+## Advanced Path Targeting
+
+When working with custom resources that have complex structures, you can use `target` to specify where template output should be placed. This is particularly useful for resources with nested specifications.
+
+```yaml
+{% include 'manifest-advanced-path.yaml' %}
+```
+
+The `target` field accepts dot-notation paths like `spec.database` or `spec.logging` to place the rendered template output at specific locations in the resource structure. When `target` is not specified it defaults to `Data` for backward compatibility with Secrets.
+
+## Drift Detection
+
+The operator automatically detects and corrects manual changes to managed custom resources. If you modify a ConfigMap or custom resource that is managed by an ExternalSecret, the operator will restore it to the desired state during the next reconciliation cycle.
+
+This ensures that your configuration remains consistent with what is defined in your external secret provider, preventing configuration drift.
+
+## Metadata and Labels
+
+You can add labels and annotations to your target resources using the template metadata:
+
+```yaml
+{% include 'manifest-labeled-configmap.yaml' %}
+```
+
+The operator automatically adds the `externalsecrets.external-secrets.io/managed: "true"` label to track which resources it manages.
+
+## RBAC Requirements
+
+When using custom resource targets, ensure the External Secrets Operator has appropriate RBAC permissions to create and manage those resources. The Helm chart provides configuration options to enable these permissions:
+
+```yaml
+nonSecretTargets:
+  enabled: true
+  rbac:
+    configMaps: true
+    customResources:
+    - apiGroups: ["config.example.com"]
+      resources: ["appconfigs"]
+```
+
+Without these permissions, the operator will not be able to create or update your target resources.

+ 43 - 0
docs/snippets/manifest-advanced-path.yaml

@@ -0,0 +1,43 @@
+{% raw %}
+apiVersion: external-secrets.io/v1
+kind: ExternalSecret
+metadata:
+  name: custom-resource-config
+spec:
+  refreshInterval: 1h
+  secretStoreRef:
+    name: vault-backend
+    kind: SecretStore
+  target:
+    name: app-settings
+    manifest:
+      apiVersion: config.example.com/v1
+      kind: AppConfig
+    template:
+      engineVersion: v2
+      templateFrom:
+      - literal: |
+          host: {{ .dbHost }}
+          port: {{ .dbPort }}
+          credentials:
+            username: {{ .dbUser }}
+            password: {{ .dbPassword }}
+        target: spec.database
+      - literal: |
+          level: info
+          format: json
+        target: spec.logging
+  data:
+  - secretKey: dbHost
+    remoteRef:
+      key: database/host
+  - secretKey: dbPort
+    remoteRef:
+      key: database/port
+  - secretKey: dbUser
+    remoteRef:
+      key: database/username
+  - secretKey: dbPassword
+    remoteRef:
+      key: database/password
+{% endraw %}

+ 19 - 0
docs/snippets/manifest-argocd-app.yaml

@@ -0,0 +1,19 @@
+{% raw %}
+apiVersion: external-secrets.io/v1
+kind: ExternalSecret
+metadata:
+  name: argocd-app
+spec:
+  refreshInterval: 15m
+  secretStoreRef:
+    name: vault-backend
+    kind: SecretStore
+  target:
+    name: my-application
+    manifest:
+      apiVersion: argoproj.io/v1alpha1
+      kind: Application
+  dataFrom:
+  - extract:
+      key: argocd/applications/my-app
+{% endraw %}

+ 23 - 0
docs/snippets/manifest-basic-configmap.yaml

@@ -0,0 +1,23 @@
+{% raw %}
+apiVersion: external-secrets.io/v1
+kind: ExternalSecret
+metadata:
+  name: application-config
+spec:
+  refreshInterval: 1h
+  secretStoreRef:
+    name: vault-backend
+    kind: SecretStore
+  target:
+    name: app-config
+    manifest:
+      apiVersion: v1
+      kind: ConfigMap
+  data:
+  - secretKey: database-host
+    remoteRef:
+      key: config/database/host
+  - secretKey: api-endpoint
+    remoteRef:
+      key: config/api/endpoint
+{% endraw %}

+ 39 - 0
docs/snippets/manifest-labeled-configmap.yaml

@@ -0,0 +1,39 @@
+{% raw %}
+apiVersion: external-secrets.io/v1
+kind: ExternalSecret
+metadata:
+  name: labeled-config
+spec:
+  refreshInterval: 1h
+  secretStoreRef:
+    name: vault-backend
+    kind: SecretStore
+  target:
+    name: labeled-configmap
+    manifest:
+      apiVersion: v1
+      kind: ConfigMap
+    template:
+      engineVersion: v2
+      metadata:
+        labels:
+          app: myapp
+          environment: production
+        annotations:
+          description: "Managed by External Secrets Operator"
+      data:
+        config.json: |
+          {
+            "feature_flags": {
+              "new_ui": {{ .featureNewUI }},
+              "beta_api": {{ .featureBetaAPI }}
+            }
+          }
+  data:
+  - secretKey: featureNewUI
+    remoteRef:
+      key: features/new-ui
+  - secretKey: featureBetaAPI
+    remoteRef:
+      key: features/beta-api
+{% endraw %}

+ 34 - 0
docs/snippets/manifest-templated-configmap.yaml

@@ -0,0 +1,34 @@
+{% raw %}
+apiVersion: external-secrets.io/v1
+kind: ExternalSecret
+metadata:
+  name: templated-config
+spec:
+  refreshInterval: 1h
+  secretStoreRef:
+    name: vault-backend
+    kind: SecretStore
+  target:
+    name: database-config
+    manifest:
+      apiVersion: v1
+      kind: ConfigMap
+    template:
+      engineVersion: v2
+      data:
+        database.yaml: |
+          host: {{ .dbHost }}
+          port: 5432
+          database: {{ .dbName }}
+          connection_string: "postgresql://user:{{ .dbPassword }}@{{ .dbHost }}:5432/{{ .dbName }}"
+  data:
+  - secretKey: dbHost
+    remoteRef:
+      key: database/hostname
+  - secretKey: dbName
+    remoteRef:
+      key: database/name
+  - secretKey: dbPassword
+    remoteRef:
+      key: database/password
+{% endraw %}

+ 33 - 0
e2e/framework/addon/eso.go

@@ -174,6 +174,39 @@ func WithCRDs() MutationFunc {
 	}
 }
 
+func WithAllowGenericTargets() MutationFunc {
+	return func(eso *ESO) {
+		eso.HelmChart.Vars = append(eso.HelmChart.Vars, StringTuple{
+			Key:   "genericTargets.enabled",
+			Value: "true",
+		}, StringTuple{
+			Key:   "genericTargets.resources[0].apiGroup",
+			Value: "",
+		}, StringTuple{
+			Key:   "genericTargets.resources[0].resources[0]",
+			Value: "configmaps",
+		}, StringTuple{
+			Key:   "genericTargets.resources[0].verbs[0]",
+			Value: "create",
+		}, StringTuple{
+			Key:   "genericTargets.resources[0].verbs[1]",
+			Value: "delete",
+		}, StringTuple{
+			Key:   "genericTargets.resources[0].verbs[2]",
+			Value: "list",
+		}, StringTuple{
+			Key:   "genericTargets.resources[0].verbs[3]",
+			Value: "get",
+		}, StringTuple{
+			Key:   "genericTargets.resources[0].verbs[4]",
+			Value: "patch",
+		}, StringTuple{
+			Key:   "genericTargets.resources[0].verbs[5]",
+			Value: "watch",
+		})
+	}
+}
+
 func (l *ESO) Install() error {
 	By("Installing eso\n")
 	err := l.HelmChart.Install()

+ 135 - 0
e2e/suites/generator/manifest.go

@@ -0,0 +1,135 @@
+/*
+Copyright © 2025 ESO Maintainer Team
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package generator
+
+import (
+	"time"
+
+	//nolint
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+
+	// nolint
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+
+	"github.com/external-secrets/external-secrets-e2e/framework"
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
+)
+
+var _ = Describe("manifest target with generator", Label("manifest"), func() {
+	f := framework.New("manifest")
+
+	var (
+		fakeGenData = map[string]string{
+			"host":     "localhost",
+			"port":     "5432",
+			"database": "mydb",
+		}
+	)
+
+	It("should template generator output into a ConfigMap", func() {
+		// Create a Fake generator
+		generator := &genv1alpha1.Fake{
+			TypeMeta: metav1.TypeMeta{
+				APIVersion: genv1alpha1.Group + "/" + genv1alpha1.Version,
+				Kind:       genv1alpha1.FakeKind,
+			},
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "manifest-generator",
+				Namespace: f.Namespace.Name,
+			},
+			Spec: genv1alpha1.FakeSpec{
+				Data: fakeGenData,
+			},
+		}
+
+		err := f.CRClient.Create(GinkgoT().Context(), generator)
+		Expect(err).ToNot(HaveOccurred())
+
+		// Create an ExternalSecret that targets a ConfigMap
+		externalSecret := &esv1.ExternalSecret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "manifest-es",
+				Namespace: f.Namespace.Name,
+			},
+			Spec: esv1.ExternalSecretSpec{
+				RefreshInterval: &metav1.Duration{Duration: time.Second * 5},
+				Target: esv1.ExternalSecretTarget{
+					Name: "generated-configmap",
+					Manifest: &esv1.ManifestReference{
+						APIVersion: "v1",
+						Kind:       "ConfigMap",
+					},
+					Template: &esv1.ExternalSecretTemplate{
+						Data: map[string]string{
+							"host":     "{{ .host }}",
+							"port":     "{{ .port }}",
+							"database": "{{ .database }}",
+						},
+					},
+				},
+				DataFrom: []esv1.ExternalSecretDataFromRemoteRef{
+					{
+						SourceRef: &esv1.StoreGeneratorSourceRef{
+							GeneratorRef: &esv1.GeneratorRef{
+								Kind: "Fake",
+								Name: "manifest-generator",
+							},
+						},
+					},
+				},
+			},
+		}
+
+		err = f.CRClient.Create(GinkgoT().Context(), externalSecret)
+		Expect(err).ToNot(HaveOccurred())
+
+		// Wait for ExternalSecret to be ready
+		Eventually(func() bool {
+			var es esv1.ExternalSecret
+			err := f.CRClient.Get(GinkgoT().Context(), types.NamespacedName{
+				Namespace: externalSecret.Namespace,
+				Name:      externalSecret.Name,
+			}, &es)
+			if err != nil {
+				return false
+			}
+
+			cond := getESCond(es.Status, esv1.ExternalSecretReady)
+			if cond == nil || cond.Status != v1.ConditionTrue {
+				return false
+			}
+			return true
+		}).WithTimeout(time.Second * 30).Should(BeTrue())
+
+		// Verify the ConfigMap was created with correct data
+		var configMap v1.ConfigMap
+		err = f.CRClient.Get(GinkgoT().Context(), types.NamespacedName{
+			Namespace: externalSecret.Namespace,
+			Name:      externalSecret.Spec.Target.Name,
+		}, &configMap)
+		Expect(err).ToNot(HaveOccurred())
+
+		// Verify data is plain text, not base64 encoded
+		Expect(configMap.Data).To(HaveKeyWithValue("host", "localhost"))
+		Expect(configMap.Data).To(HaveKeyWithValue("port", "5432"))
+		Expect(configMap.Data).To(HaveKeyWithValue("database", "mydb"))
+	})
+})

+ 1 - 1
e2e/suites/generator/suite_test.go

@@ -34,7 +34,7 @@ var _ = SynchronizedBeforeSuite(func() []byte {
 	cfg.KubeConfig, cfg.KubeClientSet, cfg.CRClient = util.NewConfig()
 
 	By("installing eso")
-	addon.InstallGlobalAddon(addon.NewESO(addon.WithCRDs()))
+	addon.InstallGlobalAddon(addon.NewESO(addon.WithCRDs(), addon.WithAllowGenericTargets()))
 
 	return nil
 }, func([]byte) {

+ 1 - 0
hack/api-docs/mkdocs.yml

@@ -102,6 +102,7 @@ nav:
           - "Lifecycle: ownership & deletion": guides/ownership-deletion-policy.md
           - Decoding Strategies: guides/decoding-strategy.md
           - Controller Classes: guides/controller-class.md
+      - Targeting Custom Resources: guides/targeting-custom-resources.md
       - Generators: guides/generator.md
       - Push Secrets: guides/pushsecrets.md
       - Operations:

+ 217 - 23
pkg/controllers/externalsecret/externalsecret_controller.go

@@ -54,14 +54,12 @@ import (
 	// Metrics.
 	"github.com/external-secrets/external-secrets/pkg/controllers/externalsecret/esmetrics"
 	ctrlmetrics "github.com/external-secrets/external-secrets/pkg/controllers/metrics"
-	"github.com/external-secrets/external-secrets/pkg/controllers/util"
+	ctrlutil "github.com/external-secrets/external-secrets/pkg/controllers/util"
 	"github.com/external-secrets/external-secrets/runtime/esutils"
 	"github.com/external-secrets/external-secrets/runtime/esutils/resolvers"
 
 	// Loading registered generators.
 	_ "github.com/external-secrets/external-secrets/pkg/register"
-	// Loading registered providers.
-	_ "github.com/external-secrets/external-secrets/pkg/register"
 )
 
 const (
@@ -130,7 +128,10 @@ var (
 	ErrSecretRemoveCtrlRef = fmt.Errorf("could not remove controller reference on secret")
 )
 
-const indexESTargetSecretNameField = ".metadata.targetSecretName"
+const (
+	indexESTargetSecretNameField = ".metadata.targetSecretName"
+	indexESTargetResourceField   = ".spec.target.resource"
+)
 
 // Reconciler reconciles a ExternalSecret object.
 type Reconciler struct {
@@ -144,7 +145,11 @@ type Reconciler struct {
 	ClusterSecretStoreEnabled bool
 	EnableFloodGate           bool
 	EnableGeneratorState      bool
+	AllowGenericTargets       bool
 	recorder                  record.EventRecorder
+
+	// informerManager manages dynamic informers for generic targets
+	informerManager InformerManager
 }
 
 // Reconcile implements the main reconciliation loop
@@ -195,6 +200,18 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ct
 			return ctrl.Result{}, err
 		}
 
+		// Release informer for generic targets
+		if isGenericTarget(externalSecret) && r.informerManager != nil {
+			gvk := getTargetGVK(externalSecret)
+			esName := types.NamespacedName{Name: externalSecret.Name, Namespace: externalSecret.Namespace}
+			if err := r.informerManager.ReleaseInformer(ctx, gvk, esName); err != nil {
+				log.Error(err, "failed to release informer for generic target",
+					"group", gvk.Group,
+					"version", gvk.Version,
+					"kind", gvk.Kind)
+			}
+		}
+
 		// Remove finalizer if it exists
 		if updated := controllerutil.RemoveFinalizer(externalSecret, ExternalSecretFinalizer); updated {
 			if err := r.Update(ctx, externalSecret); err != nil {
@@ -232,6 +249,30 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ct
 		return ctrl.Result{}, nil
 	}
 
+	// if this is a generic target, use a different reconciliation path
+	if isGenericTarget(externalSecret) {
+		// update the status of the ExternalSecret when this function returns, if needed
+		currentStatus := *externalSecret.Status.DeepCopy()
+		defer func() {
+			if equality.Semantic.DeepEqual(currentStatus, externalSecret.Status) {
+				return
+			}
+
+			updateErr := r.Status().Update(ctx, externalSecret)
+			if updateErr != nil && !apierrors.IsConflict(updateErr) {
+				log.Error(updateErr, logErrorUpdateESStatus)
+			}
+		}()
+
+		// validate generic target configuration early
+		if err := r.validateGenericTarget(log, externalSecret); err != nil {
+			r.markAsFailed("invalid generic target", err, externalSecret, syncCallsError.With(resourceLabels), esv1.ConditionReasonSecretSyncedError)
+			return ctrl.Result{}, nil // don't requeue as this is a configuration error that is not recoverable
+		}
+
+		return r.reconcileGenericTarget(ctx, externalSecret, log, start, resourceLabels, syncCallsError)
+	}
+
 	// the target secret name defaults to the ExternalSecret name, if not explicitly set
 	secretName := externalSecret.Spec.Target.Name
 	if secretName == "" {
@@ -352,7 +393,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ct
 	// retrieve the provider secret data.
 	dataMap, err := r.GetProviderSecretData(ctx, externalSecret)
 	if err != nil {
-		r.markAsFailed(msgErrorGetSecretData, err, externalSecret, syncCallsError.With(resourceLabels))
+		r.markAsFailed(msgErrorGetSecretData, err, externalSecret, syncCallsError.With(resourceLabels), esv1.ConditionReasonSecretSyncedError)
 		return ctrl.Result{}, err
 	}
 
@@ -367,7 +408,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ct
 			creationPolicy := externalSecret.Spec.Target.CreationPolicy
 			if creationPolicy != esv1.CreatePolicyOwner {
 				err = fmt.Errorf(errDeleteCreatePolicy, secretName, creationPolicy)
-				r.markAsFailed(msgErrorDeleteSecret, err, externalSecret, syncCallsError.With(resourceLabels))
+				r.markAsFailed(msgErrorDeleteSecret, err, externalSecret, syncCallsError.With(resourceLabels), esv1.ConditionReasonSecretSyncedError)
 				return ctrl.Result{}, nil
 			}
 
@@ -375,7 +416,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ct
 			if existingSecret.UID != "" {
 				err = r.Delete(ctx, existingSecret)
 				if err != nil && !apierrors.IsNotFound(err) {
-					r.markAsFailed(msgErrorDeleteSecret, err, externalSecret, syncCallsError.With(resourceLabels))
+					r.markAsFailed(msgErrorDeleteSecret, err, externalSecret, syncCallsError.With(resourceLabels), esv1.ConditionReasonSecretSyncedError)
 					return ctrl.Result{}, err
 				}
 				r.recorder.Event(externalSecret, v1.EventTypeNormal, esv1.ReasonDeleted, eventDeleted)
@@ -513,7 +554,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ct
 		// for example, if the target secret name was changed
 		err = r.deleteOrphanedSecrets(ctx, externalSecret, secretName)
 		if err != nil {
-			r.markAsFailed(msgErrorDeleteOrphaned, err, externalSecret, syncCallsError.With(resourceLabels))
+			r.markAsFailed(msgErrorDeleteOrphaned, err, externalSecret, syncCallsError.With(resourceLabels), esv1.ConditionReasonSecretSyncedError)
 			return ctrl.Result{}, err
 		}
 
@@ -535,25 +576,25 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ct
 		// detect errors indicating that we failed to set ourselves as the owner of the secret
 		// NOTE: this error cant be fixed by retrying so we don't return an error (which would requeue immediately)
 		if errors.Is(err, ErrSecretSetCtrlRef) {
-			r.markAsFailed(msgErrorBecomeOwner, err, externalSecret, syncCallsError.With(resourceLabels))
+			r.markAsFailed(msgErrorBecomeOwner, err, externalSecret, syncCallsError.With(resourceLabels), esv1.ConditionReasonSecretSyncedError)
 			return ctrl.Result{}, nil
 		}
 
 		// detect errors indicating that the secret has another ExternalSecret as owner
 		// NOTE: this error cant be fixed by retrying so we don't return an error (which would requeue immediately)
 		if errors.Is(err, ErrSecretIsOwned) {
-			r.markAsFailed(msgErrorIsOwned, err, externalSecret, syncCallsError.With(resourceLabels))
+			r.markAsFailed(msgErrorIsOwned, err, externalSecret, syncCallsError.With(resourceLabels), esv1.ConditionReasonSecretSyncedError)
 			return ctrl.Result{}, nil
 		}
 
 		// detect errors indicating that the secret is immutable
 		// NOTE: this error cant be fixed by retrying so we don't return an error (which would requeue immediately)
 		if errors.Is(err, ErrSecretImmutable) {
-			r.markAsFailed(msgErrorUpdateImmutable, err, externalSecret, syncCallsError.With(resourceLabels))
+			r.markAsFailed(msgErrorUpdateImmutable, err, externalSecret, syncCallsError.With(resourceLabels), esv1.ConditionReasonSecretSyncedError)
 			return ctrl.Result{}, nil
 		}
 
-		r.markAsFailed(msgErrorUpdateSecret, err, externalSecret, syncCallsError.With(resourceLabels))
+		r.markAsFailed(msgErrorUpdateSecret, err, externalSecret, syncCallsError.With(resourceLabels), esv1.ConditionReasonSecretSyncedError)
 		return ctrl.Result{}, err
 	}
 
@@ -561,6 +602,119 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ct
 	return r.getRequeueResult(externalSecret), nil
 }
 
+// reconcileGenericTarget handles reconciliation for generic targets (ConfigMaps, Custom Resources).
+func (r *Reconciler) reconcileGenericTarget(ctx context.Context, externalSecret *esv1.ExternalSecret, log logr.Logger, start time.Time, resourceLabels map[string]string, syncCallsError *prometheus.CounterVec) (ctrl.Result, error) {
+	// retrieve the provider secret data
+	dataMap, err := r.GetProviderSecretData(ctx, externalSecret)
+	if err != nil {
+		r.markAsFailed(msgErrorGetSecretData, err, externalSecret, syncCallsError.With(resourceLabels), esv1.ConditionReasonResourceSyncedError)
+		return ctrl.Result{}, err
+	}
+
+	// if no data was found, handle it according to deletion policy
+	if len(dataMap) == 0 {
+		switch externalSecret.Spec.Target.DeletionPolicy {
+		case esv1.DeletionPolicyDelete:
+			// safeguard that we only can delete resources we own
+			creationPolicy := externalSecret.Spec.Target.CreationPolicy
+			if creationPolicy != esv1.CreatePolicyOwner {
+				err = fmt.Errorf("unable to delete resource: creationPolicy=%s is not Owner", creationPolicy)
+				r.markAsFailed("could not delete resource", err, externalSecret, syncCallsError.With(resourceLabels), esv1.ConditionReasonResourceSyncedError)
+				return ctrl.Result{}, nil
+			}
+
+			// delete the resource if it exists
+			err = r.deleteGenericResource(ctx, log, externalSecret)
+			if err != nil {
+				r.markAsFailed("could not delete resource", err, externalSecret, syncCallsError.With(resourceLabels), esv1.ConditionReasonResourceSyncedError)
+				return ctrl.Result{}, err
+			}
+
+			r.markAsDone(externalSecret, start, log, esv1.ConditionReasonResourceDeleted, msgDeleted)
+			return r.getRequeueResult(externalSecret), nil
+
+		case esv1.DeletionPolicyRetain:
+			r.markAsDone(externalSecret, start, log, esv1.ConditionReasonResourceSynced, msgSyncedRetain)
+			return r.getRequeueResult(externalSecret), nil
+
+		case esv1.DeletionPolicyMerge:
+			// continue to process with empty data
+		}
+	}
+
+	// render the template for the manifest
+	obj, err := r.applyTemplateToManifest(ctx, externalSecret, dataMap)
+	if err != nil {
+		r.markAsFailed("could not apply template to manifest", err, externalSecret, syncCallsError.With(resourceLabels), esv1.ConditionReasonResourceSyncedError)
+		return ctrl.Result{}, err
+	}
+
+	// handle creation policies
+	switch externalSecret.Spec.Target.CreationPolicy {
+	case esv1.CreatePolicyNone:
+		log.V(1).Info("resource creation skipped due to CreationPolicy=None")
+		err = nil
+
+	case esv1.CreatePolicyMerge:
+		// for Merge policy, only update if resource exists
+		existing, getErr := r.getGenericResource(ctx, log, externalSecret)
+		if getErr != nil && !apierrors.IsNotFound(getErr) {
+			r.markAsFailed("could not get target resource", getErr, externalSecret, syncCallsError.With(resourceLabels), esv1.ConditionReasonResourceSyncedError)
+			return ctrl.Result{}, getErr
+		}
+
+		if existing == nil || existing.GetUID() == "" {
+			r.markAsDone(externalSecret, start, log, esv1.ConditionReasonResourceMissing, "resource will not be created due to CreationPolicy=Merge")
+			return r.getRequeueResult(externalSecret), nil
+		}
+
+		obj.SetResourceVersion(existing.GetResourceVersion())
+		obj.SetUID(existing.GetUID())
+
+		// update the existing resource
+		err = r.updateGenericResource(ctx, log, externalSecret, obj)
+	case esv1.CreatePolicyOrphan, esv1.CreatePolicyOwner:
+		existing, getErr := r.getGenericResource(ctx, log, externalSecret)
+		if getErr != nil && !apierrors.IsNotFound(getErr) {
+			r.markAsFailed("could not get target resource", getErr, externalSecret, syncCallsError.With(resourceLabels), esv1.ConditionReasonResourceSyncedError)
+			return ctrl.Result{}, getErr
+		}
+
+		if existing != nil {
+			obj.SetResourceVersion(existing.GetResourceVersion())
+			obj.SetUID(existing.GetUID())
+			err = r.updateGenericResource(ctx, log, externalSecret, obj)
+		} else {
+			err = r.createGenericResource(ctx, log, externalSecret, obj)
+		}
+	}
+
+	if err != nil {
+		// if we got an update conflict, requeue immediately
+		if apierrors.IsConflict(err) {
+			log.V(1).Info("conflict while updating resource, will requeue")
+			return ctrl.Result{RequeueAfter: 1 * time.Second}, nil
+		}
+
+		r.markAsFailed(msgErrorUpdateSecret, err, externalSecret, syncCallsError.With(resourceLabels), esv1.ConditionReasonResourceSyncedError)
+		return ctrl.Result{}, err
+	}
+
+	// Ensure an informer exists for this GVK to enable drift detection (only if not already managed)
+	gvk := getTargetGVK(externalSecret)
+	esName := types.NamespacedName{Name: externalSecret.Name, Namespace: externalSecret.Namespace}
+	if _, err := r.informerManager.EnsureInformer(ctx, gvk, esName); err != nil {
+		// Log the error but don't fail reconciliation - the resource was successfully created/updated
+		log.Error(err, "failed to register informer for generic target, drift detection may not work",
+			"group", gvk.Group,
+			"version", gvk.Version,
+			"kind", gvk.Kind)
+	}
+
+	r.markAsDone(externalSecret, start, log, esv1.ConditionReasonResourceSynced, msgSynced)
+	return r.getRequeueResult(externalSecret), nil
+}
+
 // getRequeueResult create a result with requeueAfter based on the ExternalSecret refresh interval.
 func (r *Reconciler) getRequeueResult(externalSecret *esv1.ExternalSecret) ctrl.Result {
 	// default to the global requeue interval
@@ -620,20 +774,26 @@ func (r *Reconciler) markAsDone(externalSecret *esv1.ExternalSecret, start time.
 	}
 }
 
-func (r *Reconciler) markAsFailed(msg string, err error, externalSecret *esv1.ExternalSecret, counter prometheus.Counter) {
+func (r *Reconciler) markAsFailed(msg string, err error, externalSecret *esv1.ExternalSecret, counter prometheus.Counter, reason string) {
 	r.recorder.Event(externalSecret, v1.EventTypeWarning, esv1.ReasonUpdateFailed, err.Error())
-	conditionSynced := NewExternalSecretCondition(esv1.ExternalSecretReady, v1.ConditionFalse, esv1.ConditionReasonSecretSyncedError, msg)
+	conditionSynced := NewExternalSecretCondition(esv1.ExternalSecretReady, v1.ConditionFalse, reason, msg)
 	SetExternalSecretCondition(externalSecret, *conditionSynced)
 	counter.Inc()
 }
 
 func (r *Reconciler) cleanupManagedSecrets(ctx context.Context, log logr.Logger, externalSecret *esv1.ExternalSecret) error {
-	// Only delete secrets if DeletionPolicy is Delete
+	// Only delete resources if DeletionPolicy is Delete
 	if externalSecret.Spec.Target.DeletionPolicy != esv1.DeletionPolicyDelete {
-		log.V(1).Info("skipping secret deletion due to DeletionPolicy", "policy", externalSecret.Spec.Target.DeletionPolicy)
+		log.V(1).Info("skipping resource deletion due to DeletionPolicy", "policy", externalSecret.Spec.Target.DeletionPolicy)
 		return nil
 	}
 
+	// if this is a generic target, use deleteGenericResource
+	if isGenericTarget(externalSecret) {
+		return r.deleteGenericResource(ctx, log, externalSecret)
+	}
+
+	// handle Secret deletion
 	secretName := externalSecret.Spec.Target.Name
 	if secretName == "" {
 		secretName = externalSecret.Name
@@ -697,8 +857,10 @@ func (r *Reconciler) createSecret(ctx context.Context, mutationFunc func(secret
 	// define and mutate the new secret
 	newSecret := &v1.Secret{
 		ObjectMeta: metav1.ObjectMeta{
-			Name:      secretName,
-			Namespace: es.Namespace,
+			Name:        secretName,
+			Namespace:   es.Namespace,
+			Labels:      map[string]string{},
+			Annotations: map[string]string{},
 		},
 		Data: make(map[string][]byte),
 	}
@@ -992,13 +1154,21 @@ func isSecretValid(existingSecret *v1.Secret, es *esv1.ExternalSecret) bool {
 }
 
 // SetupWithManager returns a new controller builder that will be started by the provided Manager.
-func (r *Reconciler) SetupWithManager(mgr ctrl.Manager, opts controller.Options) error {
+func (r *Reconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, opts controller.Options) error {
 	r.recorder = mgr.GetEventRecorderFor("external-secrets")
+	// Initialize informer manager only if generic targets are allowed
+	if r.AllowGenericTargets && r.informerManager == nil {
+		r.informerManager = NewInformerManager(ctx, mgr.GetCache(), r.Client, r.Log.WithName("informer-manager"))
+	}
 
 	// index ExternalSecrets based on the target secret name,
 	// this lets us quickly find all ExternalSecrets which target a specific Secret
-	if err := mgr.GetFieldIndexer().IndexField(context.Background(), &esv1.ExternalSecret{}, indexESTargetSecretNameField, func(obj client.Object) []string {
+	if err := mgr.GetFieldIndexer().IndexField(ctx, &esv1.ExternalSecret{}, indexESTargetSecretNameField, func(obj client.Object) []string {
 		es := obj.(*esv1.ExternalSecret)
+		// Don't index generic targets here (they use indexESTargetResourceField)
+		if isGenericTarget(es) {
+			return nil
+		}
 		// if the target name is set, use that as the index
 		if es.Spec.Target.Name != "" {
 			return []string{es.Spec.Target.Name}
@@ -1009,13 +1179,30 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager, opts controller.Options)
 		return err
 	}
 
+	// index ExternalSecrets based on the target resource (GVK + name)
+	// this lets us quickly find all ExternalSecrets which target a specific generic resource
+	if err := mgr.GetFieldIndexer().IndexField(ctx, &esv1.ExternalSecret{}, indexESTargetResourceField, func(obj client.Object) []string {
+		es := obj.(*esv1.ExternalSecret)
+		if !r.AllowGenericTargets || !isGenericTarget(es) {
+			return nil
+		}
+
+		gvk := getTargetGVK(es)
+		targetName := getTargetName(es)
+		// Index format: "group/version/kind/name"
+		return []string{fmt.Sprintf("%s/%s/%s/%s", gvk.Group, gvk.Version, gvk.Kind, targetName)}
+	}); err != nil {
+		return err
+	}
+
 	// predicate function to ignore secret events unless they have the "managed" label
 	secretHasESLabel := predicate.NewPredicateFuncs(func(object client.Object) bool {
 		value, hasLabel := object.GetLabels()[esv1.LabelManaged]
 		return hasLabel && value == esv1.LabelManagedValue
 	})
 
-	return ctrl.NewControllerManagedBy(mgr).
+	// Build the controller
+	builder := ctrl.NewControllerManagedBy(mgr).
 		WithOptions(opts).
 		For(&esv1.ExternalSecret{}).
 		// we cant use Owns(), as we don't set ownerReferences when the creationPolicy is not Owner.
@@ -1024,8 +1211,15 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager, opts controller.Options)
 			&v1.Secret{},
 			handler.EnqueueRequestsFromMapFunc(r.findObjectsForSecret),
 			builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}, secretHasESLabel),
-		).
-		Complete(r)
+		)
+
+	// Watch generic targets dynamically via the informer manager
+	// Only add this watch source if the feature is enabled
+	if r.AllowGenericTargets {
+		builder = builder.WatchesRawSource(r.informerManager.Source())
+	}
+
+	return builder.Complete(r)
 }
 
 func (r *Reconciler) findObjectsForSecret(ctx context.Context, secret client.Object) []reconcile.Request {

+ 295 - 0
pkg/controllers/externalsecret/externalsecret_controller_manifest.go

@@ -0,0 +1,295 @@
+/*
+Copyright © 2025 ESO Maintainer Team
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package externalsecret
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/go-logr/logr"
+	v1 "k8s.io/api/core/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+	"github.com/external-secrets/external-secrets/pkg/controllers/templating"
+	"github.com/external-secrets/external-secrets/runtime/template"
+)
+
+// isGenericTarget checks if the ExternalSecret targets a generic resource.
+func isGenericTarget(es *esv1.ExternalSecret) bool {
+	return es.Spec.Target.Manifest != nil
+}
+
+// validateGenericTarget validates that generic targets are properly configured.
+func (r *Reconciler) validateGenericTarget(log logr.Logger, es *esv1.ExternalSecret) error {
+	if !r.AllowGenericTargets {
+		return fmt.Errorf("generic targets are disabled. Enable with --unsafe-allow-generic-targets flag")
+	}
+
+	manifest := es.Spec.Target.Manifest
+	if manifest.APIVersion == "" {
+		return fmt.Errorf("target.manifest.apiVersion is required")
+	}
+	if manifest.Kind == "" {
+		return fmt.Errorf("target.manifest.kind is required")
+	}
+
+	log.Info("Warning: Using generic target. Make sure access policies and encryption are properly configured.",
+		"apiVersion", manifest.APIVersion,
+		"kind", manifest.Kind,
+		"name", getTargetName(es))
+
+	return nil
+}
+
+// getTargetGVK returns the GroupVersionKind for the target resource.
+func getTargetGVK(es *esv1.ExternalSecret) schema.GroupVersionKind {
+	manifest := es.Spec.Target.Manifest
+	gv, _ := schema.ParseGroupVersion(manifest.APIVersion)
+
+	return schema.GroupVersionKind{
+		Group:   gv.Group,
+		Version: gv.Version,
+		Kind:    manifest.Kind,
+	}
+}
+
+// getTargetName returns the name of the target resource.
+func getTargetName(es *esv1.ExternalSecret) string {
+	if es.Spec.Target.Name != "" {
+		return es.Spec.Target.Name
+	}
+	return es.Name
+}
+
+// getGenericResource retrieves a generic resource using the controller-runtime client.
+func (r *Reconciler) getGenericResource(ctx context.Context, log logr.Logger, es *esv1.ExternalSecret) (*unstructured.Unstructured, error) {
+	gvk := getTargetGVK(es)
+
+	resource := &unstructured.Unstructured{}
+	resource.SetGroupVersionKind(gvk)
+
+	err := r.Client.Get(ctx, client.ObjectKey{
+		Namespace: es.Namespace,
+		Name:      getTargetName(es),
+	}, resource)
+
+	if err != nil {
+		if apierrors.IsNotFound(err) {
+			log.V(1).Info("target resource does not exist", "gvk", gvk.String(), "name", getTargetName(es))
+			return nil, err
+		}
+		return nil, fmt.Errorf("failed to get target resource: %w", err)
+	}
+
+	return resource, nil
+}
+
+func (r *Reconciler) createGenericResource(ctx context.Context, log logr.Logger, es *esv1.ExternalSecret, obj *unstructured.Unstructured) error {
+	gvk := getTargetGVK(es)
+
+	// Check if resource already exists
+	existing := &unstructured.Unstructured{}
+	existing.SetGroupVersionKind(gvk)
+	err := r.Client.Get(ctx, client.ObjectKey{
+		Namespace: es.Namespace,
+		Name:      getTargetName(es),
+	}, existing)
+
+	if err != nil {
+		if !apierrors.IsNotFound(err) {
+			return fmt.Errorf("failed to check if target resource exists: %w", err)
+		}
+	} else {
+		return fmt.Errorf("target resource with name %s already exists", getTargetName(es))
+	}
+
+	log.Info("creating target resource", "gvk", gvk.String(), "name", getTargetName(es))
+	err = r.Client.Create(ctx, obj)
+	if err != nil {
+		return fmt.Errorf("failed to create target resource: %w", err)
+	}
+
+	r.recorder.Event(es, v1.EventTypeNormal, "Created", fmt.Sprintf("Created %s %s", gvk.Kind, getTargetName(es)))
+	return nil
+}
+
+func (r *Reconciler) updateGenericResource(ctx context.Context, log logr.Logger, es *esv1.ExternalSecret, existing *unstructured.Unstructured) error {
+	gvk := getTargetGVK(es)
+
+	log.Info("updating target resource", "gvk", gvk.String(), "name", getTargetName(es))
+	err := r.Client.Update(ctx, existing)
+	if err != nil {
+		return fmt.Errorf("failed to update target resource: %w", err)
+	}
+
+	r.recorder.Event(es, v1.EventTypeNormal, "Updated", fmt.Sprintf("Updated %s %s", gvk.Kind, getTargetName(es)))
+	return nil
+}
+
+// deleteGenericResource deletes a generic resource.
+func (r *Reconciler) deleteGenericResource(ctx context.Context, log logr.Logger, es *esv1.ExternalSecret) error {
+	if !r.AllowGenericTargets || !isGenericTarget(es) {
+		return nil
+	}
+
+	gvk := getTargetGVK(es)
+
+	obj := &unstructured.Unstructured{}
+	obj.SetGroupVersionKind(gvk)
+	obj.SetNamespace(es.Namespace)
+	obj.SetName(getTargetName(es))
+
+	log.Info("deleting target resource", "gvk", gvk.String(), "name", getTargetName(es))
+	err := r.Client.Delete(ctx, obj)
+	if err != nil && !apierrors.IsNotFound(err) {
+		return fmt.Errorf("failed to delete target resource: %w", err)
+	}
+
+	r.recorder.Event(es, v1.EventTypeNormal, "Deleted", fmt.Sprintf("Deleted %s %s", gvk.Kind, getTargetName(es)))
+	return nil
+}
+
+// applyTemplateToManifest renders templates for generic resources and returns an unstructured object.
+func (r *Reconciler) applyTemplateToManifest(ctx context.Context, es *esv1.ExternalSecret, dataMap map[string][]byte) (*unstructured.Unstructured, error) {
+	gvk := getTargetGVK(es)
+
+	obj := &unstructured.Unstructured{}
+	obj.SetGroupVersionKind(gvk)
+	obj.SetName(getTargetName(es))
+	obj.SetNamespace(es.Namespace)
+
+	labels := make(map[string]string)
+	annotations := make(map[string]string)
+
+	if es.Spec.Target.Template != nil {
+		for k, v := range es.Spec.Target.Template.Metadata.Labels {
+			labels[k] = v
+		}
+		for k, v := range es.Spec.Target.Template.Metadata.Annotations {
+			annotations[k] = v
+		}
+	}
+
+	labels[esv1.LabelManaged] = esv1.LabelManagedValue
+
+	obj.SetLabels(labels)
+	obj.SetAnnotations(annotations)
+
+	if es.Spec.Target.Template == nil {
+		return r.createSimpleManifest(obj, dataMap)
+	}
+
+	return r.renderTemplatedManifest(ctx, es, obj, dataMap)
+}
+
+// createSimpleManifest creates a simple resource without templates (e.g., ConfigMap with data field).
+func (r *Reconciler) createSimpleManifest(obj *unstructured.Unstructured, dataMap map[string][]byte) (*unstructured.Unstructured, error) {
+	// For ConfigMaps and similar resources, put data in .data field
+	if obj.GetKind() == "ConfigMap" {
+		data := make(map[string]string)
+		for k, v := range dataMap {
+			data[k] = string(v)
+		}
+		obj.Object["data"] = data
+
+		return obj, nil
+	}
+
+	// For other resources, put in spec.data or just data
+	data := make(map[string]string)
+	for k, v := range dataMap {
+		data[k] = string(v)
+	}
+	if obj.Object["spec"] == nil {
+		obj.Object["spec"] = make(map[string]any)
+	}
+	spec := obj.Object["spec"].(map[string]any)
+	spec["data"] = data
+
+	return obj, nil
+}
+
+// renderTemplatedManifest renders templates for a custom resource.
+func (r *Reconciler) renderTemplatedManifest(ctx context.Context, es *esv1.ExternalSecret, obj *unstructured.Unstructured, dataMap map[string][]byte) (*unstructured.Unstructured, error) {
+	execute, err := template.EngineForVersion(es.Spec.Target.Template.EngineVersion)
+	if err != nil {
+		return nil, fmt.Errorf("failed to get template engine: %w", err)
+	}
+
+	// Handle templateFrom entries
+	for _, tplFrom := range es.Spec.Target.Template.TemplateFrom {
+		targetPath := tplFrom.Target
+		if targetPath == "" {
+			targetPath = esv1.TemplateTargetData
+		}
+
+		if tplFrom.Literal != nil {
+			// Execute template directly against the unstructured object
+			out := make(map[string][]byte)
+			out[*tplFrom.Literal] = []byte(*tplFrom.Literal)
+			if err := execute(out, dataMap, esv1.TemplateScopeKeysAndValues, targetPath, obj); err != nil {
+				return nil, fmt.Errorf("failed to execute literal template: %w", err)
+			}
+		}
+
+		if tplFrom.ConfigMap != nil || tplFrom.Secret != nil {
+			// Parser still uses v1.Secret, so collect data and apply via template engine to the end result.
+			tempSecret := &v1.Secret{Data: make(map[string][]byte)}
+			p := templating.Parser{
+				Client:       r.Client,
+				TargetSecret: tempSecret,
+				DataMap:      dataMap,
+				Exec:         execute,
+			}
+
+			if tplFrom.ConfigMap != nil {
+				if err := p.MergeConfigMap(ctx, es.Namespace, tplFrom); err != nil {
+					return nil, fmt.Errorf("failed to merge configmap template: %w", err)
+				}
+			}
+
+			if tplFrom.Secret != nil {
+				if err := p.MergeSecret(ctx, es.Namespace, tplFrom); err != nil {
+					return nil, fmt.Errorf("failed to merge secret template: %w", err)
+				}
+			}
+
+			// apply collected data to the target object
+			if err := execute(tempSecret.Data, dataMap, esv1.TemplateScopeValues, targetPath, obj); err != nil {
+				return nil, fmt.Errorf("failed to apply merged templates to path %s: %w", targetPath, err)
+			}
+		}
+	}
+
+	// Handle template.data entries
+	if len(es.Spec.Target.Template.Data) > 0 {
+		tplMap := make(map[string][]byte)
+		for k, v := range es.Spec.Target.Template.Data {
+			tplMap[k] = []byte(v)
+		}
+
+		if err := execute(tplMap, dataMap, esv1.TemplateScopeValues, esv1.TemplateTargetData, obj); err != nil {
+			return nil, fmt.Errorf("failed to execute template.data: %w", err)
+		}
+	}
+
+	return obj, nil
+}

+ 629 - 0
pkg/controllers/externalsecret/externalsecret_controller_manifest_test.go

@@ -0,0 +1,629 @@
+/*
+Copyright © 2025 ESO Maintainer Team
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package externalsecret
+
+import (
+	"context"
+	"testing"
+
+	"github.com/go-logr/logr"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	v1 "k8s.io/api/core/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/client-go/kubernetes/scheme"
+	"k8s.io/utils/ptr"
+	ctrl "sigs.k8s.io/controller-runtime"
+	fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+)
+
+func TestIsGenericTarget(t *testing.T) {
+	tests := []struct {
+		name     string
+		es       *esv1.ExternalSecret
+		expected bool
+	}{
+		{
+			name: "nil manifest - Secret target",
+			es: &esv1.ExternalSecret{
+				Spec: esv1.ExternalSecretSpec{
+					Target: esv1.ExternalSecretTarget{
+						Manifest: nil,
+					},
+				},
+			},
+			expected: false,
+		},
+		{
+			name: "ConfigMap manifest target",
+			es: &esv1.ExternalSecret{
+				Spec: esv1.ExternalSecretSpec{
+					Target: esv1.ExternalSecretTarget{
+						Manifest: &esv1.ManifestReference{
+							APIVersion: "v1",
+							Kind:       "ConfigMap",
+						},
+					},
+				},
+			},
+			expected: true,
+		},
+		{
+			name: "Custom Resource manifest target",
+			es: &esv1.ExternalSecret{
+				Spec: esv1.ExternalSecretSpec{
+					Target: esv1.ExternalSecretTarget{
+						Manifest: &esv1.ManifestReference{
+							APIVersion: "argoproj.io/v1alpha1",
+							Kind:       "Application",
+						},
+					},
+				},
+			},
+			expected: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := isGenericTarget(tt.es)
+			assert.Equal(t, tt.expected, result)
+		})
+	}
+}
+
+func TestValidateGenericTarget(t *testing.T) {
+	tests := []struct {
+		name                string
+		es                  *esv1.ExternalSecret
+		allowGenericTargets bool
+		expectedError       bool
+		errorContains       string
+	}{
+		{
+			name: "ConfigMap target - flag enabled - valid",
+			es: &esv1.ExternalSecret{
+				Spec: esv1.ExternalSecretSpec{
+					Target: esv1.ExternalSecretTarget{
+						Manifest: &esv1.ManifestReference{
+							APIVersion: "v1",
+							Kind:       "ConfigMap",
+						},
+					},
+				},
+			},
+			allowGenericTargets: true,
+			expectedError:       false,
+		},
+		{
+			name: "ConfigMap target - flag disabled",
+			es: &esv1.ExternalSecret{
+				Spec: esv1.ExternalSecretSpec{
+					Target: esv1.ExternalSecretTarget{
+						Manifest: &esv1.ManifestReference{
+							APIVersion: "v1",
+							Kind:       "ConfigMap",
+						},
+					},
+				},
+			},
+			allowGenericTargets: false,
+			expectedError:       true,
+			errorContains:       "generic targets are disabled",
+		},
+		{
+			name: "Missing APIVersion",
+			es: &esv1.ExternalSecret{
+				Spec: esv1.ExternalSecretSpec{
+					Target: esv1.ExternalSecretTarget{
+						Manifest: &esv1.ManifestReference{
+							APIVersion: "",
+							Kind:       "ConfigMap",
+						},
+					},
+				},
+			},
+			allowGenericTargets: true,
+			expectedError:       true,
+			errorContains:       "apiVersion is required",
+		},
+		{
+			name: "Missing Kind",
+			es: &esv1.ExternalSecret{
+				Spec: esv1.ExternalSecretSpec{
+					Target: esv1.ExternalSecretTarget{
+						Manifest: &esv1.ManifestReference{
+							APIVersion: "v1",
+							Kind:       "",
+						},
+					},
+				},
+			},
+			allowGenericTargets: true,
+			expectedError:       true,
+			errorContains:       "kind is required",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			r := &Reconciler{
+				AllowGenericTargets: tt.allowGenericTargets,
+			}
+			log := ctrl.Log.WithName("test")
+
+			err := r.validateGenericTarget(log, tt.es)
+
+			if tt.expectedError {
+				assert.Error(t, err)
+				if tt.errorContains != "" {
+					assert.Contains(t, err.Error(), tt.errorContains)
+				}
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestGetTargetGVK(t *testing.T) {
+	tests := []struct {
+		name     string
+		es       *esv1.ExternalSecret
+		expected schema.GroupVersionKind
+	}{
+		{
+			name: "ConfigMap target",
+			es: &esv1.ExternalSecret{
+				Spec: esv1.ExternalSecretSpec{
+					Target: esv1.ExternalSecretTarget{
+						Manifest: &esv1.ManifestReference{
+							APIVersion: "v1",
+							Kind:       "ConfigMap",
+						},
+					},
+				},
+			},
+			expected: schema.GroupVersionKind{
+				Group:   "",
+				Version: "v1",
+				Kind:    "ConfigMap",
+			},
+		},
+		{
+			name: "ArgoCD Application target",
+			es: &esv1.ExternalSecret{
+				Spec: esv1.ExternalSecretSpec{
+					Target: esv1.ExternalSecretTarget{
+						Manifest: &esv1.ManifestReference{
+							APIVersion: "argoproj.io/v1alpha1",
+							Kind:       "Application",
+						},
+					},
+				},
+			},
+			expected: schema.GroupVersionKind{
+				Group:   "argoproj.io",
+				Version: "v1alpha1",
+				Kind:    "Application",
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := getTargetGVK(tt.es)
+			assert.Equal(t, tt.expected, result)
+		})
+	}
+}
+
+func TestGetTargetName(t *testing.T) {
+	tests := []struct {
+		name     string
+		es       *esv1.ExternalSecret
+		expected string
+	}{
+		{
+			name: "Use target name when specified",
+			es: &esv1.ExternalSecret{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "my-external-secret",
+				},
+				Spec: esv1.ExternalSecretSpec{
+					Target: esv1.ExternalSecretTarget{
+						Name: "custom-target-name",
+					},
+				},
+			},
+			expected: "custom-target-name",
+		},
+		{
+			name: "Use ExternalSecret name when target name not specified",
+			es: &esv1.ExternalSecret{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "my-external-secret",
+				},
+				Spec: esv1.ExternalSecretSpec{
+					Target: esv1.ExternalSecretTarget{
+						Name: "",
+					},
+				},
+			},
+			expected: "my-external-secret",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := getTargetName(tt.es)
+			assert.Equal(t, tt.expected, result)
+		})
+	}
+}
+
+func TestCreateSimpleManifest(t *testing.T) {
+	tests := []struct {
+		name     string
+		kind     string
+		dataMap  map[string][]byte
+		validate func(t *testing.T, obj *unstructured.Unstructured)
+	}{
+		{
+			name: "ConfigMap with data",
+			kind: "ConfigMap",
+			dataMap: map[string][]byte{
+				"key1": []byte("value1"),
+				"key2": []byte("value2"),
+			},
+			validate: func(t *testing.T, obj *unstructured.Unstructured) {
+				// Directly access the data field
+				data, ok := obj.Object["data"].(map[string]string)
+				require.True(t, ok, "data should be map[string]string")
+				assert.Equal(t, "value1", data["key1"])
+				assert.Equal(t, "value2", data["key2"])
+			},
+		},
+		{
+			name: "Custom resource with spec.data",
+			kind: "CustomResource",
+			dataMap: map[string][]byte{
+				"config": []byte("my-config"),
+			},
+			validate: func(t *testing.T, obj *unstructured.Unstructured) {
+				spec, ok := obj.Object["spec"].(map[string]interface{})
+				require.True(t, ok, "spec should be map[string]interface{}")
+				data, ok := spec["data"].(map[string]string)
+				require.True(t, ok, "spec.data should be map[string]string")
+				assert.Equal(t, "my-config", data["config"])
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			r := &Reconciler{}
+			obj := &unstructured.Unstructured{
+				Object: make(map[string]interface{}),
+			}
+			obj.SetKind(tt.kind)
+
+			result, err := r.createSimpleManifest(obj, tt.dataMap)
+
+			require.NoError(t, err)
+			assert.NotNil(t, result)
+			if tt.validate != nil {
+				tt.validate(t, result)
+			}
+		})
+	}
+}
+
+func TestApplyTemplateToManifest_SimpleConfigMap(t *testing.T) {
+	// Setup
+	_ = esv1.AddToScheme(scheme.Scheme)
+	fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme.Scheme).Build()
+
+	r := &Reconciler{
+		Client: fakeClient,
+	}
+
+	es := &esv1.ExternalSecret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "test-es",
+			Namespace: "default",
+		},
+		Spec: esv1.ExternalSecretSpec{
+			Target: esv1.ExternalSecretTarget{
+				Name: "test-configmap",
+				Manifest: &esv1.ManifestReference{
+					APIVersion: "v1",
+					Kind:       "ConfigMap",
+				},
+			},
+		},
+	}
+
+	dataMap := map[string][]byte{
+		"key1": []byte("value1"),
+		"key2": []byte("value2"),
+	}
+
+	// Execute
+	result, err := r.applyTemplateToManifest(context.Background(), es, dataMap)
+
+	// Verify
+	require.NoError(t, err)
+	assert.NotNil(t, result)
+	assert.Equal(t, "ConfigMap", result.GetKind())
+	assert.Equal(t, "test-configmap", result.GetName())
+	assert.Equal(t, "default", result.GetNamespace())
+
+	// Verify data
+	data, ok := result.Object["data"].(map[string]string)
+	require.True(t, ok, "data should be map[string]string")
+	assert.Equal(t, "value1", data["key1"])
+	assert.Equal(t, "value2", data["key2"])
+
+	// Verify managed label
+	labels := result.GetLabels()
+	assert.Equal(t, esv1.LabelManagedValue, labels[esv1.LabelManaged])
+}
+
+func TestApplyTemplateToManifest_WithMetadata(t *testing.T) {
+	// Setup
+	_ = esv1.AddToScheme(scheme.Scheme)
+	fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme.Scheme).Build()
+
+	r := &Reconciler{
+		Client: fakeClient,
+	}
+
+	es := &esv1.ExternalSecret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "test-es",
+			Namespace: "default",
+		},
+		Spec: esv1.ExternalSecretSpec{
+			Target: esv1.ExternalSecretTarget{
+				Name: "test-configmap",
+				Manifest: &esv1.ManifestReference{
+					APIVersion: "v1",
+					Kind:       "ConfigMap",
+				},
+				Template: &esv1.ExternalSecretTemplate{
+					EngineVersion: esv1.TemplateEngineV2, // Set engine version
+					Metadata: esv1.ExternalSecretTemplateMetadata{
+						Labels: map[string]string{
+							"app":  "myapp",
+							"tier": "backend",
+						},
+						Annotations: map[string]string{
+							"description": "This is a test",
+						},
+					},
+				},
+			},
+		},
+	}
+
+	dataMap := map[string][]byte{
+		"config": []byte("test-config"),
+	}
+
+	// Execute
+	result, err := r.applyTemplateToManifest(context.Background(), es, dataMap)
+
+	// Verify
+	require.NoError(t, err)
+	assert.NotNil(t, result)
+
+	// Verify labels
+	labels := result.GetLabels()
+	assert.Equal(t, "myapp", labels["app"])
+	assert.Equal(t, "backend", labels["tier"])
+	assert.Equal(t, esv1.LabelManagedValue, labels[esv1.LabelManaged])
+
+	// Verify annotations
+	annotations := result.GetAnnotations()
+	assert.Equal(t, "This is a test", annotations["description"])
+}
+
+func TestGetGenericResource(t *testing.T) {
+	// Setup
+	_ = esv1.AddToScheme(scheme.Scheme)
+
+	// Create a ConfigMap to find
+	existingConfigMap := &unstructured.Unstructured{
+		Object: map[string]interface{}{
+			"apiVersion": "v1",
+			"kind":       "ConfigMap",
+			"metadata": map[string]interface{}{
+				"name":      "test-cm",
+				"namespace": "default",
+			},
+			"data": map[string]interface{}{
+				"key": "value",
+			},
+		},
+	}
+
+	_ = esv1.AddToScheme(scheme.Scheme)
+	fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(existingConfigMap).Build()
+
+	r := &Reconciler{
+		Client: fakeClient,
+	}
+
+	es := &esv1.ExternalSecret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "test-es",
+			Namespace: "default",
+		},
+		Spec: esv1.ExternalSecretSpec{
+			Target: esv1.ExternalSecretTarget{
+				Name: "test-cm",
+				Manifest: &esv1.ManifestReference{
+					APIVersion: "v1",
+					Kind:       "ConfigMap",
+				},
+			},
+		},
+	}
+
+	// Execute
+	result, err := r.getGenericResource(context.Background(), logr.Discard(), es)
+
+	// Verify
+	require.NoError(t, err)
+	assert.NotNil(t, result)
+	assert.Equal(t, "ConfigMap", result.GetKind())
+	assert.Equal(t, "test-cm", result.GetName())
+
+	// Verify data
+	data, found, err := unstructured.NestedStringMap(result.Object, "data")
+	require.NoError(t, err)
+	require.True(t, found)
+	assert.Equal(t, "value", data["key"])
+}
+
+func TestGetGenericResource_NotFound(t *testing.T) {
+	// Setup
+	_ = esv1.AddToScheme(scheme.Scheme)
+	fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme.Scheme).Build()
+
+	r := &Reconciler{
+		Client: fakeClient,
+	}
+
+	es := &esv1.ExternalSecret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "test-es",
+			Namespace: "default",
+		},
+		Spec: esv1.ExternalSecretSpec{
+			Target: esv1.ExternalSecretTarget{
+				Name: "nonexistent-cm",
+				Manifest: &esv1.ManifestReference{
+					APIVersion: "v1",
+					Kind:       "ConfigMap",
+				},
+			},
+		},
+	}
+
+	// Execute
+	result, err := r.getGenericResource(context.Background(), logr.Discard(), es)
+
+	// Verify - should return an error and nil result when resource doesn't exist
+	assert.Error(t, err)
+	assert.True(t, apierrors.IsNotFound(err))
+	assert.Nil(t, result)
+}
+
+func init() {
+	// Initialize scheme for tests
+	_ = esv1.AddToScheme(scheme.Scheme)
+	_ = v1.AddToScheme(scheme.Scheme)
+}
+
+func TestApplyTemplateToManifest_LiteralWithDeployment(t *testing.T) {
+	// Test that literal templates work with complex objects like Deployments
+	_ = esv1.AddToScheme(scheme.Scheme)
+	fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme.Scheme).Build()
+
+	r := &Reconciler{
+		Client: fakeClient,
+	}
+
+	es := &esv1.ExternalSecret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "test-es",
+			Namespace: "default",
+		},
+		Spec: esv1.ExternalSecretSpec{
+			Target: esv1.ExternalSecretTarget{
+				Name: "test-deployment",
+				Manifest: &esv1.ManifestReference{
+					APIVersion: "apps/v1",
+					Kind:       "Deployment",
+				},
+				Template: &esv1.ExternalSecretTemplate{
+					EngineVersion: esv1.TemplateEngineV2,
+					TemplateFrom: []esv1.TemplateFrom{
+						{
+							Target: "spec",
+							Literal: ptr.To(`
+replicas: {{ .replicas }}
+selector:
+  matchLabels:
+    app: myapp
+template:
+  metadata:
+    labels:
+      app: myapp
+  spec:
+    containers:
+    - name: nginx
+      image: nginx:{{ .version }}
+      ports:
+      - containerPort: 80
+`),
+						},
+					},
+				},
+			},
+		},
+	}
+
+	dataMap := map[string][]byte{
+		"replicas": []byte("3"),
+		"version":  []byte("1.21"),
+	}
+
+	result, err := r.applyTemplateToManifest(context.Background(), es, dataMap)
+
+	require.NoError(t, err)
+	assert.NotNil(t, result)
+	assert.Equal(t, "Deployment", result.GetKind())
+	assert.Equal(t, "test-deployment", result.GetName())
+
+	spec, found, err := unstructured.NestedMap(result.Object, "spec")
+	require.NoError(t, err)
+	require.True(t, found, "spec should exist")
+
+	replicas, found, err := unstructured.NestedInt64(result.Object, "spec", "replicas")
+	require.NoError(t, err)
+	require.True(t, found, "spec.replicas should exist")
+	assert.Equal(t, int64(3), replicas)
+
+	containers, found, err := unstructured.NestedSlice(result.Object, "spec", "template", "spec", "containers")
+	require.NoError(t, err)
+	require.True(t, found, "containers should exist")
+	require.Len(t, containers, 1, "should have 1 container")
+
+	container, ok := containers[0].(map[string]any)
+	require.True(t, ok, "container should be a map")
+	assert.Equal(t, "nginx:1.21", container["image"])
+
+	t.Logf("Result spec: %+v", spec)
+}

+ 156 - 0
pkg/controllers/externalsecret/externalsecret_controller_watch_test.go

@@ -0,0 +1,156 @@
+/*
+Copyright © 2025 ESO Maintainer Team
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package externalsecret
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+)
+
+func TestGetTargetResourceIndex(t *testing.T) {
+	tests := []struct {
+		name           string
+		es             *esv1.ExternalSecret
+		expectedValues []string
+	}{
+		{
+			name: "ConfigMap target",
+			es: &esv1.ExternalSecret{
+				Spec: esv1.ExternalSecretSpec{
+					Target: esv1.ExternalSecretTarget{
+						Name: "my-configmap",
+						Manifest: &esv1.ManifestReference{
+							APIVersion: "v1",
+							Kind:       "ConfigMap",
+						},
+					},
+				},
+			},
+			expectedValues: []string{"/v1/ConfigMap/my-configmap"},
+		},
+		{
+			name: "ArgoCD Application target",
+			es: &esv1.ExternalSecret{
+				Spec: esv1.ExternalSecretSpec{
+					Target: esv1.ExternalSecretTarget{
+						Name: "my-app",
+						Manifest: &esv1.ManifestReference{
+							APIVersion: "argoproj.io/v1alpha1",
+							Kind:       "Application",
+						},
+					},
+				},
+			},
+			expectedValues: []string{"argoproj.io/v1alpha1/Application/my-app"},
+		},
+		{
+			name: "Secret target (no manifest)",
+			es: &esv1.ExternalSecret{
+				Spec: esv1.ExternalSecretSpec{
+					Target: esv1.ExternalSecretTarget{
+						Name:     "my-secret",
+						Manifest: nil,
+					},
+				},
+			},
+			expectedValues: nil, // Secrets don't get indexed for generic resources
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if !isGenericTarget(tt.es) {
+				assert.Nil(t, tt.expectedValues)
+				return
+			}
+
+			gvk := getTargetGVK(tt.es)
+			targetName := getTargetName(tt.es)
+			indexValue := gvk.Group + "/" + gvk.Version + "/" + gvk.Kind + "/" + targetName
+
+			require.Len(t, tt.expectedValues, 1)
+			assert.Equal(t, tt.expectedValues[0], indexValue)
+		})
+	}
+}
+
+func TestGVKFromManifestTarget(t *testing.T) {
+	tests := []struct {
+		name        string
+		manifest    *esv1.ManifestReference
+		expectedGVK schema.GroupVersionKind
+	}{
+		{
+			name: "Core v1 ConfigMap",
+			manifest: &esv1.ManifestReference{
+				APIVersion: "v1",
+				Kind:       "ConfigMap",
+			},
+			expectedGVK: schema.GroupVersionKind{
+				Group:   "",
+				Version: "v1",
+				Kind:    "ConfigMap",
+			},
+		},
+		{
+			name: "ArgoCD Application",
+			manifest: &esv1.ManifestReference{
+				APIVersion: "argoproj.io/v1alpha1",
+				Kind:       "Application",
+			},
+			expectedGVK: schema.GroupVersionKind{
+				Group:   "argoproj.io",
+				Version: "v1alpha1",
+				Kind:    "Application",
+			},
+		},
+		{
+			name: "Networking v1 Ingress",
+			manifest: &esv1.ManifestReference{
+				APIVersion: "networking.k8s.io/v1",
+				Kind:       "Ingress",
+			},
+			expectedGVK: schema.GroupVersionKind{
+				Group:   "networking.k8s.io",
+				Version: "v1",
+				Kind:    "Ingress",
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			es := &esv1.ExternalSecret{
+				Spec: esv1.ExternalSecretSpec{
+					Target: esv1.ExternalSecretTarget{
+						Manifest: tt.manifest,
+					},
+				},
+			}
+
+			gvk := getTargetGVK(es)
+			assert.Equal(t, tt.expectedGVK.Group, gvk.Group)
+			assert.Equal(t, tt.expectedGVK.Version, gvk.Version)
+			assert.Equal(t, tt.expectedGVK.Kind, gvk.Kind)
+		})
+	}
+}

+ 310 - 0
pkg/controllers/externalsecret/informer_manager.go

@@ -0,0 +1,310 @@
+/*
+Copyright © 2025 ESO Maintainer Team
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package externalsecret
+
+import (
+	"context"
+	"fmt"
+	"sync"
+
+	"github.com/go-logr/logr"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/fields"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/client-go/util/workqueue"
+	ctrl "sigs.k8s.io/controller-runtime"
+	runtimecache "sigs.k8s.io/controller-runtime/pkg/cache"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/reconcile"
+	"sigs.k8s.io/controller-runtime/pkg/source"
+
+	esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
+)
+
+// InformerManager manages the lifecycle of informers for generic target resources.
+// It handles dynamic registration, tracking, and cleanup of informers.
+type InformerManager interface {
+	// EnsureInformer ensures an informer exists for the given GVK and registers the ExternalSecret as using it.
+	// Returns true if a new informer was created, false if it already existed.
+	EnsureInformer(ctx context.Context, gvk schema.GroupVersionKind, es types.NamespacedName) (bool, error)
+
+	// ReleaseInformer unregisters the ExternalSecret from using this GVK.
+	// If no more ExternalSecrets use this GVK, the informer is stopped and removed.
+	ReleaseInformer(ctx context.Context, gvk schema.GroupVersionKind, es types.NamespacedName) error
+
+	// IsManaged returns true if the manager is currently managing an informer for the GVK.
+	IsManaged(gvk schema.GroupVersionKind) bool
+
+	// GetInformer returns the informer for a GVK if it exists.
+	GetInformer(gvk schema.GroupVersionKind) (runtimecache.Informer, bool)
+
+	// Source returns a source.TypedSource that can be used with WatchesRawSource
+	Source() source.TypedSource[reconcile.Request]
+
+	// SetQueue binds the reconcile queue to the informer manager
+	SetQueue(queue workqueue.TypedRateLimitingInterface[ctrl.Request]) error
+}
+
+// informerEntry tracks an informer and the ExternalSecrets using it.
+type informerEntry struct {
+	informer runtimecache.Informer
+	// externalSecrets tracks the external secrets using a GVK. Once this list is empty, we
+	// stop the informer and deregister it to free up resources. It is a map instead of just a number to prevent
+	// duplicated reconcile ensures to increase the number on each reconcile.
+	externalSecrets map[types.NamespacedName]struct{}
+}
+
+// DefaultInformerManager implements InformerManager using controller-runtime's cache.
+type DefaultInformerManager struct {
+	cache          runtimecache.Cache
+	client         client.Client
+	log            logr.Logger
+	mu             sync.RWMutex
+	informers      map[string]*informerEntry // key: GVK string
+	queue          workqueue.TypedRateLimitingInterface[ctrl.Request]
+	managerContext context.Context
+}
+
+// NewInformerManager creates a new InformerManager.
+func NewInformerManager(ctx context.Context, cache runtimecache.Cache, client client.Client, log logr.Logger) InformerManager {
+	return &DefaultInformerManager{
+		managerContext: ctx,
+		cache:          cache,
+		client:         client,
+		log:            log,
+		informers:      make(map[string]*informerEntry),
+	}
+}
+
+// EnsureInformer ensures an informer exists for the given GVK and registers the ExternalSecret.
+func (m *DefaultInformerManager) EnsureInformer(ctx context.Context, gvk schema.GroupVersionKind, es types.NamespacedName) (bool, error) {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	key := gvk.String()
+
+	// check if we have this gvk in the list of informers already
+	if entry, exists := m.informers[key]; exists {
+		// register this ExternalSecret as using this informer (deduplicate);
+		entry.externalSecrets[es] = struct{}{}
+		m.log.Info("registered ExternalSecret with existing informer",
+			"gvk", key,
+			"externalSecret", es,
+			"totalUsers", len(entry.externalSecrets))
+		return false, nil
+	}
+
+	if m.queue == nil {
+		return false, fmt.Errorf("queue not initialized, call SetQueue first")
+	}
+
+	// Get or create informer for this GVK
+	informer, err := m.cache.GetInformerForKind(ctx, gvk)
+	if err != nil {
+		return false, fmt.Errorf("failed to get informer for %s: %w", key, err)
+	}
+
+	// Add event handler to the informer that enqueues reconcile requests
+	_, err = informer.AddEventHandler(&enqueueHandler{
+		managerContext: m.managerContext,
+		gvk:            gvk,
+		client:         m.client,
+		queue:          m.queue,
+		log:            m.log,
+	})
+	if err != nil {
+		return false, fmt.Errorf("failed to add event handler for %s: %w", key, err)
+	}
+
+	// Store the informer with this ExternalSecret as the first user
+	m.informers[key] = &informerEntry{
+		informer:        informer,
+		externalSecrets: map[types.NamespacedName]struct{}{es: {}},
+	}
+
+	m.log.Info("registered informer for generic target",
+		"group", gvk.Group,
+		"version", gvk.Version,
+		"kind", gvk.Kind,
+		"externalSecret", es)
+
+	return true, nil
+}
+
+// enqueueHandler is an event handler that enqueues reconcile requests for ExternalSecrets
+// that target the changed resource.
+type enqueueHandler struct {
+	managerContext context.Context
+	gvk            schema.GroupVersionKind
+	client         client.Client
+	queue          workqueue.TypedRateLimitingInterface[ctrl.Request]
+	log            logr.Logger
+}
+
+func (h *enqueueHandler) OnAdd(obj interface{}, _ bool) {
+	h.enqueue(obj)
+}
+
+func (h *enqueueHandler) OnUpdate(_, newObj interface{}) {
+	h.enqueue(newObj)
+}
+
+func (h *enqueueHandler) OnDelete(obj interface{}) {
+	h.enqueue(obj)
+}
+
+func (h *enqueueHandler) enqueue(obj interface{}) {
+	// Extract metadata
+	meta, ok := obj.(metav1.Object)
+	if !ok {
+		h.log.Error(nil, "unexpected object type", "type", fmt.Sprintf("%T", obj))
+		return
+	}
+
+	// Only process resources with the managed label
+	labels := meta.GetLabels()
+	if labels == nil {
+		return
+	}
+
+	value, hasLabel := labels[esv1.LabelManaged]
+	if !hasLabel || value != esv1.LabelManagedValue {
+		return
+	}
+
+	// Find ExternalSecrets that target this resource
+	externalSecretsList := &esv1.ExternalSecretList{}
+	indexValue := fmt.Sprintf("%s/%s/%s/%s", h.gvk.Group, h.gvk.Version, h.gvk.Kind, meta.GetName())
+	listOps := &client.ListOptions{
+		FieldSelector: fields.OneTermEqualSelector(indexESTargetResourceField, indexValue),
+		Namespace:     meta.GetNamespace(),
+	}
+
+	if err := h.client.List(h.managerContext, externalSecretsList, listOps); err != nil {
+		h.log.Error(err, "failed to list ExternalSecrets for resource",
+			"gvk", h.gvk.String(),
+			"name", meta.GetName(),
+			"namespace", meta.GetNamespace())
+		return
+	}
+
+	// Enqueue reconcile requests for each ExternalSecret
+	for i := range externalSecretsList.Items {
+		req := ctrl.Request{
+			NamespacedName: types.NamespacedName{
+				Name:      externalSecretsList.Items[i].GetName(),
+				Namespace: externalSecretsList.Items[i].GetNamespace(),
+			},
+		}
+		h.queue.Add(req)
+
+		h.log.V(1).Info("enqueued reconcile request due to resource change",
+			"externalSecret", req.NamespacedName,
+			"targetGVK", h.gvk.String(),
+			"targetResource", meta.GetName())
+	}
+}
+
+// ReleaseInformer unregisters the ExternalSecret from using this GVK.
+func (m *DefaultInformerManager) ReleaseInformer(ctx context.Context, gvk schema.GroupVersionKind, es types.NamespacedName) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	key := gvk.String()
+
+	entry, exists := m.informers[key]
+	if !exists {
+		// Already removed or never existed; can happen if we had a bad start, failed to watch, or during other errors.
+		// In that case, there is nothing else to do really.
+		m.log.Info("informer not found for release",
+			"gvk", key,
+			"externalSecret", es)
+		return nil
+	}
+
+	// remove the ES from the list of ESs using this GVK
+	delete(entry.externalSecrets, es)
+	m.log.Info("unregistered ExternalSecret from informer",
+		"gvk", key,
+		"externalSecret", es,
+		"remainingUsers", len(entry.externalSecrets))
+
+	// if no more ExternalSecrets are using this informer, remove it
+	if len(entry.externalSecrets) == 0 {
+		partial := &metav1.PartialObjectMetadata{}
+		partial.SetGroupVersionKind(gvk)
+
+		if err := m.cache.RemoveInformer(ctx, partial); err != nil {
+			m.log.Error(err, "failed to remove informer, will clean up tracking anyway",
+				"gvk", key)
+		}
+
+		delete(m.informers, key)
+
+		m.log.Info("removed informer for generic target (no more users)",
+			"group", gvk.Group,
+			"version", gvk.Version,
+			"kind", gvk.Kind)
+	}
+
+	return nil
+}
+
+// IsManaged returns true if the manager is currently managing an informer for the GVK.
+func (m *DefaultInformerManager) IsManaged(gvk schema.GroupVersionKind) bool {
+	m.mu.RLock()
+	defer m.mu.RUnlock()
+
+	_, exists := m.informers[gvk.String()]
+	return exists
+}
+
+// GetInformer returns the informer for a GVK if it exists.
+func (m *DefaultInformerManager) GetInformer(gvk schema.GroupVersionKind) (runtimecache.Informer, bool) {
+	m.mu.RLock()
+	defer m.mu.RUnlock()
+
+	entry, exists := m.informers[gvk.String()]
+	if !exists {
+		return nil, false
+	}
+	return entry.informer, true
+}
+
+// Source returns a source.TypedSource that binds the reconcile queue to this manager.
+func (m *DefaultInformerManager) Source() source.TypedSource[reconcile.Request] {
+	return source.Func(func(_ context.Context, queue workqueue.TypedRateLimitingInterface[ctrl.Request]) error {
+		// This dynamically binds the given queue to the informer manager
+		// From this point on, the queue will receive events for all registered informers
+		return m.SetQueue(queue)
+	})
+}
+
+// SetQueue binds the reconcile queue to the informer manager.
+func (m *DefaultInformerManager) SetQueue(queue workqueue.TypedRateLimitingInterface[ctrl.Request]) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	if m.queue != nil {
+		return fmt.Errorf("queue already set")
+	}
+
+	m.queue = queue
+	m.log.Info("reconcile queue bound to informer manager")
+	return nil
+}

+ 1 - 1
pkg/controllers/externalsecret/suite_test.go

@@ -117,7 +117,7 @@ var _ = BeforeSuite(func() {
 		Log:                       ctrl.Log.WithName("controllers").WithName("ExternalSecrets"),
 		RequeueInterval:           time.Second,
 		ClusterSecretStoreEnabled: true,
-	}).SetupWithManager(k8sManager, controller.Options{
+	}).SetupWithManager(ctx, k8sManager, controller.Options{
 		MaxConcurrentReconciles: 1,
 		RateLimiter:             ctrlcommon.BuildRateLimiter(),
 	})

+ 1 - 1
pkg/controllers/templating/parser.go

@@ -164,7 +164,7 @@ func (p *Parser) MergeTemplateFrom(ctx context.Context, namespace string, templa
 }
 
 // MergeMap merges the given map of templates into the target secret.
-func (p *Parser) MergeMap(tplMap map[string]string, target esv1.TemplateTarget) error {
+func (p *Parser) MergeMap(tplMap map[string]string, target string) error {
 	byteMap := make(map[string][]byte)
 	for k, v := range tplMap {
 		byteMap[k] = []byte(v)

+ 2 - 2
runtime/template/engine.go

@@ -20,14 +20,14 @@ package template
 import (
 	"fmt"
 
-	corev1 "k8s.io/api/core/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	esapi "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
 	v2 "github.com/external-secrets/external-secrets/runtime/template/v2"
 )
 
 // ExecFunc is the function signature type for executing a template engine.
-type ExecFunc func(tpl, data map[string][]byte, scope esapi.TemplateScope, target esapi.TemplateTarget, secret *corev1.Secret) error
+type ExecFunc func(tpl, data map[string][]byte, scope esapi.TemplateScope, target string, secret client.Object) error
 
 // EngineForVersion returns the appropriate template engine for the given version.
 func EngineForVersion(version esapi.TemplateEngineVersion) (ExecFunc, error) {

+ 196 - 24
runtime/template/v2/template.go

@@ -19,12 +19,15 @@ package template
 import (
 	"bytes"
 	"fmt"
+	"strings"
 	tpl "text/template"
 
 	"github.com/Masterminds/sprig/v3"
 	"github.com/spf13/pflag"
 	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/util/yaml"
+	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	esapi "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
 	"github.com/external-secrets/external-secrets/runtime/feature"
@@ -58,6 +61,11 @@ var tplFuncs = tpl.FuncMap{
 
 var leftDelim, rightDelim string
 
+var (
+	errConvertingToUnstructured = "failed to convert object to unstructured: %w"
+	errConvertingToObject       = "failed to convert unstructured to object: %w"
+)
+
 // FuncMap returns the template function map so other templating calls can use the same extra functions.
 func FuncMap() tpl.FuncMap {
 	return tplFuncs
@@ -90,56 +98,129 @@ func init() {
 	})
 }
 
-func applyToTarget(k string, val []byte, target esapi.TemplateTarget, secret *corev1.Secret) {
+func applyToTarget(k string, val []byte, target string, obj client.Object) error {
+	target = strings.ToLower(target)
 	switch target {
-	case esapi.TemplateTargetAnnotations:
-		if secret.Annotations == nil {
-			secret.Annotations = make(map[string]string)
+	case "annotations":
+		annotations := obj.GetAnnotations()
+		if annotations == nil {
+			annotations = make(map[string]string)
+		}
+		annotations[k] = string(val)
+		obj.SetAnnotations(annotations)
+	case "labels":
+		labels := obj.GetLabels()
+		if labels == nil {
+			labels = make(map[string]string)
 		}
-		secret.Annotations[k] = string(val)
-	case esapi.TemplateTargetLabels:
-		if secret.Labels == nil {
-			secret.Labels = make(map[string]string)
+		labels[k] = string(val)
+		obj.SetLabels(labels)
+	case "data":
+		if err := setField(obj, "data", k, val); err != nil {
+			return fmt.Errorf("failed to set data field on object: %w", err)
 		}
-		secret.Labels[k] = string(val)
-	case esapi.TemplateTargetData:
-		if secret.Data == nil {
-			secret.Data = make(map[string][]byte)
+	case "spec":
+		if err := setField(obj, "spec", k, val); err != nil {
+			return fmt.Errorf("failed to set data field on object: %w", err)
 		}
-		secret.Data[k] = val
 	default:
+		parts := strings.Split(target, ".")
+		if len(parts) == 0 {
+			return fmt.Errorf("invalid path: %s", target)
+		}
+
+		unstructured, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
+		if err != nil {
+			return fmt.Errorf(errConvertingToUnstructured, err)
+		}
+
+		// Navigate to the parent of the target field
+		current := unstructured
+		for i := range len(parts) - 1 {
+			part := parts[i]
+			if current[part] == nil {
+				current[part] = make(map[string]any)
+			}
+			next, ok := current[part].(map[string]any)
+			if !ok {
+				return fmt.Errorf("path %s is not a map at segment %s", target, part)
+			}
+			current = next
+		}
+
+		// Set the value at the final key
+		// Convert []byte to string to avoid base64 encoding when serializing
+		lastPart := parts[len(parts)-1]
+		current[lastPart] = tryParseYAML(string(val))
+
+		// Convert back to the original object type
+		if err := runtime.DefaultUnstructuredConverter.FromUnstructured(unstructured, obj); err != nil {
+			return fmt.Errorf(errConvertingToObject, err)
+		}
+	}
+
+	// all fields have been nilled out if they weren't set.
+	if obj.GetLabels() == nil {
+		obj.SetLabels(make(map[string]string))
+	}
+	if obj.GetAnnotations() == nil {
+		obj.SetAnnotations(make(map[string]string))
 	}
+
+	return nil
 }
 
-func valueScopeApply(tplMap, data map[string][]byte, target esapi.TemplateTarget, secret *corev1.Secret) error {
+func valueScopeApply(tplMap, data map[string][]byte, target string, secret client.Object) error {
 	for k, v := range tplMap {
 		val, err := execute(k, string(v), data)
 		if err != nil {
 			return fmt.Errorf(errExecute, k, err)
 		}
-		applyToTarget(k, val, target, secret)
+		if err := applyToTarget(k, val, target, secret); err != nil {
+			return fmt.Errorf("failed to apply to target: %w", err)
+		}
 	}
 	return nil
 }
 
-func mapScopeApply(tpl string, data map[string][]byte, target esapi.TemplateTarget, secret *corev1.Secret) error {
+func mapScopeApply(tpl string, data map[string][]byte, target string, secret client.Object) error {
 	val, err := execute(tpl, tpl, data)
 	if err != nil {
 		return fmt.Errorf(errExecute, tpl, err)
 	}
-	src := make(map[string]string)
-	err = yaml.Unmarshal(val, &src)
-	if err != nil {
-		return fmt.Errorf("could not unmarshal template to 'map[string][]byte': %w", err)
+
+	target = strings.ToLower(target)
+	switch target {
+	case "annotations", "labels", "data":
+		// normal route
+		src := make(map[string]string)
+		err = yaml.Unmarshal(val, &src)
+		if err != nil {
+			return fmt.Errorf("could not unmarshal template to 'map[string][]byte': %w", err)
+		}
+		for k, val := range src {
+			if err := applyToTarget(k, []byte(val), target, secret); err != nil {
+				return fmt.Errorf("failed to apply to target: %w", err)
+			}
+		}
+
+		// we are done
+		return nil
 	}
-	for k, val := range src {
-		applyToTarget(k, []byte(val), target, secret)
+
+	// for more complex path, we need to navigate to the last element of the path
+	// creating objects in that path if they don't exist and then apply the parsed
+	// structure at that location to the entire object.
+	var parsed any
+	if err := yaml.Unmarshal(val, &parsed); err != nil {
+		return fmt.Errorf("could not unmarshal template YAML: %w", err)
 	}
-	return nil
+
+	return applyParsedToPath(parsed, target, secret)
 }
 
 // Execute renders the secret data as template. If an error occurs processing is stopped immediately.
-func Execute(tpl, data map[string][]byte, scope esapi.TemplateScope, target esapi.TemplateTarget, secret *corev1.Secret) error {
+func Execute(tpl, data map[string][]byte, scope esapi.TemplateScope, target string, secret client.Object) error {
 	if tpl == nil {
 		return nil
 	}
@@ -183,3 +264,94 @@ func execute(k, val string, data map[string][]byte) ([]byte, error) {
 	}
 	return buf.Bytes(), nil
 }
+
+// setData sets the data field of the object.
+func setField(obj client.Object, field, k string, val []byte) error {
+	m, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
+	if err != nil {
+		return fmt.Errorf(errConvertingToUnstructured, err)
+	}
+	_, ok := m[field]
+	if !ok {
+		m[field] = map[string]any{}
+	}
+	specMap, ok := m[field].(map[string]any)
+	if !ok {
+		return fmt.Errorf("failed to convert data to map[string][]byte")
+	}
+
+	// Secrets require base64-encoded []byte values in the data field
+	// Other resources (ConfigMaps, custom resources) need plain string values
+	_, isSecret := obj.(*corev1.Secret)
+	if isSecret {
+		// For Secrets, keep as []byte (will be base64-encoded during serialization)
+		specMap[k] = val
+	} else {
+		// For generic (ConfigMaps, custom resources), use plain strings
+		specMap[k] = string(val)
+	}
+	m[field] = specMap
+
+	// Convert back to the original object type
+	if err := runtime.DefaultUnstructuredConverter.FromUnstructured(m, obj); err != nil {
+		return fmt.Errorf(errConvertingToObject, err)
+	}
+	return nil
+}
+
+// tryParseYAML attempts to parse a string value as YAML, returns original value if parsing fails.
+func tryParseYAML(value any) any {
+	str, ok := value.(string)
+	if !ok {
+		return value
+	}
+
+	var parsed any
+	if err := yaml.Unmarshal([]byte(str), &parsed); err == nil {
+		return parsed
+	}
+
+	return value
+}
+
+// applyParsedToPath applies a parsed YAML structure to a specific path in the object.
+func applyParsedToPath(parsed any, target string, obj client.Object) error {
+	unstructured, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
+	if err != nil {
+		return fmt.Errorf(errConvertingToUnstructured, err)
+	}
+
+	parts := strings.Split(target, ".")
+	if len(parts) == 0 {
+		return fmt.Errorf("invalid path: %s", target)
+	}
+
+	// single value, aka "spec"
+	if len(parts) == 1 {
+		unstructured[parts[0]] = parsed
+	} else {
+		// navigate to the last element of the path and apply the entire struct at that location.
+		// build up the entire map structure that we are eventually going to apply.
+		current := unstructured
+		for _, part := range parts {
+			if current[part] == nil {
+				current[part] = make(map[string]any)
+			}
+			next, ok := current[part].(map[string]any)
+			if !ok {
+				return fmt.Errorf("path %s is not a map at segment %s", target, part)
+			}
+			current = next
+		}
+
+		// once we constructed the entire segment, we finally apply our parsed object
+		current[parts[len(parts)-1]] = parsed
+	}
+
+	// convert back to original object
+	if err := runtime.DefaultUnstructuredConverter.FromUnstructured(unstructured, obj); err != nil {
+		return fmt.Errorf(errConvertingToObject, err)
+	}
+
+	return nil
+}

+ 238 - 24
runtime/template/v2/template_test.go

@@ -629,9 +629,8 @@ func TestExecute(t *testing.T) {
 		},
 	}
 
-	for i := range tbl {
-		row := tbl[i]
-		t.Run(row.name, func(t *testing.T) {
+	for _, tt := range tbl {
+		t.Run(tt.name, func(t *testing.T) {
 			sec := &corev1.Secret{
 				Data:       make(map[string][]byte),
 				StringData: make(map[string]string),
@@ -639,36 +638,36 @@ func TestExecute(t *testing.T) {
 			}
 			oldLeftDelim := leftDelim
 			oldRightDelim := rightDelim
-			if row.leftDelimiter != "" {
-				leftDelim = row.leftDelimiter
+			if tt.leftDelimiter != "" {
+				leftDelim = tt.leftDelimiter
 			}
-			if row.rightDelimiter != "" {
-				rightDelim = row.rightDelimiter
+			if tt.rightDelimiter != "" {
+				rightDelim = tt.rightDelimiter
 			}
 			defer func() {
 				leftDelim = oldLeftDelim
 				rightDelim = oldRightDelim
 			}()
-			err := Execute(row.tpl, row.data, esapi.TemplateScopeValues, esapi.TemplateTargetData, sec)
-			if !ErrorContains(err, row.expErr) {
-				t.Errorf("unexpected error: %s, expected: %s", err, row.expErr)
+			err := Execute(tt.tpl, tt.data, esapi.TemplateScopeValues, esapi.TemplateTargetData, sec)
+			if !ErrorContains(err, tt.expErr) {
+				t.Errorf("unexpected error: %s, expected: %s", err, tt.expErr)
 			}
-			err = Execute(row.labelsTpl, row.data, esapi.TemplateScopeValues, esapi.TemplateTargetLabels, sec)
-			if !ErrorContains(err, row.expLblErr) {
-				t.Errorf("unexpected error: %s, expected: %s", err, row.expErr)
+			err = Execute(tt.labelsTpl, tt.data, esapi.TemplateScopeValues, esapi.TemplateTargetLabels, sec)
+			if !ErrorContains(err, tt.expLblErr) {
+				t.Errorf("unexpected error: %s, expected: %s", err, tt.expErr)
 			}
-			err = Execute(row.annotationsTpl, row.data, esapi.TemplateScopeValues, esapi.TemplateTargetAnnotations, sec)
-			if !ErrorContains(err, row.expAnnoErr) {
-				t.Errorf("unexpected error: %s, expected: %s", err, row.expErr)
+			err = Execute(tt.annotationsTpl, tt.data, esapi.TemplateScopeValues, esapi.TemplateTargetAnnotations, sec)
+			if !ErrorContains(err, tt.expAnnoErr) {
+				t.Errorf("unexpected error: %s, expected: %s", err, tt.expErr)
 			}
-			if row.expectedData != nil {
-				assert.EqualValues(t, row.expectedData, sec.Data)
+			if tt.expectedData != nil {
+				assert.EqualValues(t, tt.expectedData, sec.Data)
 			}
-			if row.expectedLabels != nil {
-				assert.EqualValues(t, row.expectedLabels, sec.ObjectMeta.Labels)
+			if tt.expectedLabels != nil {
+				assert.EqualValues(t, tt.expectedLabels, sec.ObjectMeta.Labels)
 			}
-			if row.expectedAnnotations != nil {
-				assert.EqualValues(t, row.expectedAnnotations, sec.ObjectMeta.Annotations)
+			if tt.expectedAnnotations != nil {
+				assert.EqualValues(t, tt.expectedAnnotations, sec.ObjectMeta.Annotations)
 			}
 		})
 	}
@@ -678,7 +677,7 @@ func TestScopeValuesWithSecretFieldsNil(t *testing.T) {
 	tbl := []struct {
 		name               string
 		tpl                map[string][]byte
-		target             esapi.TemplateTarget
+		target             string
 		data               map[string][]byte
 		expectedData       map[string][]byte
 		expectedStringData map[string]string
@@ -764,7 +763,7 @@ func TestScopeKeysAndValues(t *testing.T) {
 	tbl := []struct {
 		name               string
 		tpl                map[string][]byte
-		target             esapi.TemplateTarget
+		target             string
 		data               map[string][]byte
 		expectedData       map[string][]byte
 		expectedStringData map[string]string
@@ -842,6 +841,193 @@ func TestScopeKeysAndValues(t *testing.T) {
 		})
 	}
 }
+func TestComplexYAMLFieldsWithSpec(t *testing.T) {
+	// These tests verify that the template engine can handle complex YAML values
+	// for spec fields. Since ConfigMap doesn't have a spec field in its typed definition,
+	// we test by inspecting what would be set in an unstructured representation.
+
+	type testCase struct {
+		name   string
+		tpl    map[string][]byte
+		target string
+		scope  esapi.TemplateScope
+		data   map[string][]byte
+		verify func(t *testing.T, obj *corev1.Secret)
+		expErr string
+	}
+
+	tests := []testCase{
+		{
+			name:   "data target with simple string in Secret",
+			target: "data",
+			scope:  esapi.TemplateScopeValues,
+			tpl: map[string][]byte{
+				"simple": []byte("{{ .value }}"),
+			},
+			data: map[string][]byte{
+				"value": []byte("test-value"),
+			},
+			verify: func(t *testing.T, obj *corev1.Secret) {
+				assert.Equal(t, []byte("test-value"), obj.Data["simple"])
+			},
+		},
+		{
+			name:   "data target with multiple keys",
+			target: "data",
+			scope:  esapi.TemplateScopeValues,
+			tpl: map[string][]byte{
+				"username": []byte("{{ .user }}"),
+				"password": []byte("{{ .pass }}"),
+			},
+			data: map[string][]byte{
+				"user": []byte("admin"),
+				"pass": []byte("secret123"),
+			},
+			verify: func(t *testing.T, obj *corev1.Secret) {
+				assert.Equal(t, []byte("admin"), obj.Data["username"])
+				assert.Equal(t, []byte("secret123"), obj.Data["password"])
+			},
+		},
+		{
+			name:   "labels target with templated values",
+			target: "labels",
+			scope:  esapi.TemplateScopeValues,
+			tpl: map[string][]byte{
+				"app":     []byte("{{ .app }}"),
+				"version": []byte("{{ .version }}"),
+			},
+			data: map[string][]byte{
+				"app":     []byte("my-app"),
+				"version": []byte("1.0.0"),
+			},
+			verify: func(t *testing.T, obj *corev1.Secret) {
+				assert.Equal(t, "my-app", obj.Labels["app"])
+				assert.Equal(t, "1.0.0", obj.Labels["version"])
+			},
+		},
+		{
+			name:   "annotations target with templated values",
+			target: "annotations",
+			scope:  esapi.TemplateScopeValues,
+			tpl: map[string][]byte{
+				"description": []byte("{{ .desc }}"),
+				"owner":       []byte("{{ .owner }}"),
+			},
+			data: map[string][]byte{
+				"desc":  []byte("Test secret"),
+				"owner": []byte("team-platform"),
+			},
+			verify: func(t *testing.T, obj *corev1.Secret) {
+				assert.Equal(t, "Test secret", obj.Annotations["description"])
+				assert.Equal(t, "team-platform", obj.Annotations["owner"])
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			obj := &corev1.Secret{
+				ObjectMeta: v1.ObjectMeta{
+					Name:        "test-secret",
+					Namespace:   "default",
+					Labels:      make(map[string]string),
+					Annotations: make(map[string]string),
+				},
+				Data: make(map[string][]byte),
+			}
+
+			err := Execute(tt.tpl, tt.data, tt.scope, tt.target, obj)
+
+			if tt.expErr != "" {
+				require.Error(t, err)
+				assert.Contains(t, err.Error(), tt.expErr)
+				return
+			}
+
+			require.NoError(t, err)
+			if tt.verify != nil {
+				tt.verify(t, obj)
+			}
+		})
+	}
+}
+
+func TestTryParseYAML(t *testing.T) {
+	tests := []struct {
+		name     string
+		input    any
+		expected any
+	}{
+		{
+			name:     "parse YAML object",
+			input:    "key: value\nfoo: bar",
+			expected: map[string]any{"key": "value", "foo": "bar"},
+		},
+		{
+			name:     "parse YAML array",
+			input:    "- item1\n- item2\n- item3",
+			expected: []any{"item1", "item2", "item3"},
+		},
+		{
+			name:     "parse YAML number",
+			input:    "42",
+			expected: int64(42),
+		},
+		{
+			name:     "parse YAML boolean true",
+			input:    "true",
+			expected: true,
+		},
+		{
+			name:     "parse YAML boolean false",
+			input:    "false",
+			expected: false,
+		},
+		{
+			name:     "parse YAML float",
+			input:    "3.14",
+			expected: 3.14,
+		},
+		{
+			name:     "parse complex YAML",
+			input:    "database:\n  host: localhost\n  port: 5432\n  enabled: true",
+			expected: map[string]any{"database": map[string]any{"host": "localhost", "port": int64(5432), "enabled": true}},
+		},
+		{
+			name:     "plain string returns as-is",
+			input:    "just a string",
+			expected: "just a string",
+		},
+		{
+			name:     "invalid YAML returns original",
+			input:    "invalid: : yaml:",
+			expected: "invalid: : yaml:",
+		},
+		{
+			name:     "non-string input returns as-is",
+			input:    123,
+			expected: 123,
+		},
+		{
+			name:     "byte slice returns as-is",
+			input:    []byte("test"),
+			expected: []byte("test"),
+		},
+		{
+			name:     "null/empty string",
+			input:    "",
+			expected: nil,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := tryParseYAML(tt.input)
+			assert.Equal(t, tt.expected, result)
+		})
+	}
+}
+
 func ErrorContains(out error, want string) bool {
 	if out == nil {
 		return want == ""
@@ -975,3 +1161,31 @@ func TestPkcs12certPass(t *testing.T) {
 		})
 	}
 }
+
+func TestConfigMapDataNotBase64Encoded(t *testing.T) {
+	configMap := &corev1.ConfigMap{
+		ObjectMeta: v1.ObjectMeta{
+			Name:      "test-cm",
+			Namespace: "default",
+		},
+	}
+
+	data := map[string][]byte{
+		"host":     []byte("localhost"),
+		"port":     []byte("5432"),
+		"database": []byte("mydb"),
+	}
+
+	tplMap := map[string][]byte{
+		"host":     []byte("{{ .host }}"),
+		"port":     []byte("{{ .port }}"),
+		"database": []byte("{{ .database }}"),
+	}
+
+	err := Execute(tplMap, data, esapi.TemplateScopeValues, "Data", configMap)
+	require.NoError(t, err)
+
+	assert.Equal(t, "localhost", configMap.Data["host"], "host should be plain text, not base64")
+	assert.Equal(t, "5432", configMap.Data["port"], "port should be plain text, not base64")
+	assert.Equal(t, "mydb", configMap.Data["database"], "database should be plain text, not base64")
+}

+ 3 - 0
tests/__snapshot__/clusterexternalsecret-v1.yaml

@@ -68,6 +68,9 @@ spec:
       creationPolicy: "Owner"
       deletionPolicy: "Retain"
       immutable: true
+      manifest:
+        apiVersion: external-secrets.io/v1
+        kind: string
       name: string
       template:
         data: {}

+ 3 - 0
tests/__snapshot__/externalsecret-v1.yaml

@@ -63,6 +63,9 @@ spec:
     creationPolicy: "Owner"
     deletionPolicy: "Retain"
     immutable: true
+    manifest:
+      apiVersion: external-secrets.io/v1
+      kind: string
     name: string
     template:
       data: {}