Browse Source

:sparkles: Templates from string (#1748)

* Adds templates from string

Signed-off-by: Gustavo Carvalho <gusfcarvalho@gmail.com>
Gustavo Fernandes de Carvalho 3 years ago
parent
commit
0bd9ea4dbd

+ 23 - 3
apis/externalsecrets/v1beta1/externalsecret_types.go

@@ -98,7 +98,6 @@ type ExternalSecretTemplate struct {
 
 
 	// +optional
 	// +optional
 	Data map[string]string `json:"data,omitempty"`
 	Data map[string]string `json:"data,omitempty"`
-
 	// +optional
 	// +optional
 	TemplateFrom []TemplateFrom `json:"templateFrom,omitempty"`
 	TemplateFrom []TemplateFrom `json:"templateFrom,omitempty"`
 }
 }
@@ -110,13 +109,32 @@ const (
 	TemplateEngineV2 TemplateEngineVersion = "v2"
 	TemplateEngineV2 TemplateEngineVersion = "v2"
 )
 )
 
 
-// +kubebuilder:validation:MinProperties=1
-// +kubebuilder:validation:MaxProperties=1
 type TemplateFrom struct {
 type TemplateFrom struct {
 	ConfigMap *TemplateRef `json:"configMap,omitempty"`
 	ConfigMap *TemplateRef `json:"configMap,omitempty"`
 	Secret    *TemplateRef `json:"secret,omitempty"`
 	Secret    *TemplateRef `json:"secret,omitempty"`
+	// +optional
+	// +optional
+	// +kubebuilder:default="Data"
+	Target TemplateTarget `json:"target,omitempty"`
+	// +optional
+	Literal *string `json:"literal,omitempty"`
 }
 }
 
 
+type TemplateScope string
+
+const (
+	TemplateScopeValues        TemplateScope = "Values"
+	TemplateScopeKeysAndValues TemplateScope = "KeysAndValues"
+)
+
+type TemplateTarget string
+
+const (
+	TemplateTargetData        TemplateTarget = "Data"
+	TemplateTargetAnnotations TemplateTarget = "Annotations"
+	TemplateTargetLabels      TemplateTarget = "Labels"
+)
+
 type TemplateRef struct {
 type TemplateRef struct {
 	Name  string            `json:"name"`
 	Name  string            `json:"name"`
 	Items []TemplateRefItem `json:"items"`
 	Items []TemplateRefItem `json:"items"`
@@ -124,6 +142,8 @@ type TemplateRef struct {
 
 
 type TemplateRefItem struct {
 type TemplateRefItem struct {
 	Key string `json:"key"`
 	Key string `json:"key"`
+	// +kubebuilder:default="Values"
+	TemplateAs TemplateScope `json:"templateAs,omitempty"`
 }
 }
 
 
 // ExternalSecretTarget defines the Kubernetes Secret to be created
 // ExternalSecretTarget defines the Kubernetes Secret to be created

+ 5 - 0
apis/externalsecrets/v1beta1/zz_generated.deepcopy.go

@@ -1828,6 +1828,11 @@ func (in *TemplateFrom) DeepCopyInto(out *TemplateFrom) {
 		*out = new(TemplateRef)
 		*out = new(TemplateRef)
 		(*in).DeepCopyInto(*out)
 		(*in).DeepCopyInto(*out)
 	}
 	}
+	if in.Literal != nil {
+		in, out := &in.Literal, &out.Literal
+		*out = new(string)
+		**out = **in
+	}
 }
 }
 
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TemplateFrom.
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TemplateFrom.

+ 11 - 2
config/crds/bases/external-secrets.io_clusterexternalsecrets.yaml

@@ -367,8 +367,6 @@ spec:
                             type: object
                             type: object
                           templateFrom:
                           templateFrom:
                             items:
                             items:
-                              maxProperties: 1
-                              minProperties: 1
                               properties:
                               properties:
                                 configMap:
                                 configMap:
                                   properties:
                                   properties:
@@ -377,6 +375,9 @@ spec:
                                         properties:
                                         properties:
                                           key:
                                           key:
                                             type: string
                                             type: string
+                                          templateAs:
+                                            default: Values
+                                            type: string
                                         required:
                                         required:
                                         - key
                                         - key
                                         type: object
                                         type: object
@@ -387,6 +388,8 @@ spec:
                                   - items
                                   - items
                                   - name
                                   - name
                                   type: object
                                   type: object
+                                literal:
+                                  type: string
                                 secret:
                                 secret:
                                   properties:
                                   properties:
                                     items:
                                     items:
@@ -394,6 +397,9 @@ spec:
                                         properties:
                                         properties:
                                           key:
                                           key:
                                             type: string
                                             type: string
+                                          templateAs:
+                                            default: Values
+                                            type: string
                                         required:
                                         required:
                                         - key
                                         - key
                                         type: object
                                         type: object
@@ -404,6 +410,9 @@ spec:
                                   - items
                                   - items
                                   - name
                                   - name
                                   type: object
                                   type: object
+                                target:
+                                  default: Data
+                                  type: string
                               type: object
                               type: object
                             type: array
                             type: array
                           type:
                           type:

+ 11 - 2
config/crds/bases/external-secrets.io_externalsecrets.yaml

@@ -600,8 +600,6 @@ spec:
                         type: object
                         type: object
                       templateFrom:
                       templateFrom:
                         items:
                         items:
-                          maxProperties: 1
-                          minProperties: 1
                           properties:
                           properties:
                             configMap:
                             configMap:
                               properties:
                               properties:
@@ -610,6 +608,9 @@ spec:
                                     properties:
                                     properties:
                                       key:
                                       key:
                                         type: string
                                         type: string
+                                      templateAs:
+                                        default: Values
+                                        type: string
                                     required:
                                     required:
                                     - key
                                     - key
                                     type: object
                                     type: object
@@ -620,6 +621,8 @@ spec:
                               - items
                               - items
                               - name
                               - name
                               type: object
                               type: object
+                            literal:
+                              type: string
                             secret:
                             secret:
                               properties:
                               properties:
                                 items:
                                 items:
@@ -627,6 +630,9 @@ spec:
                                     properties:
                                     properties:
                                       key:
                                       key:
                                         type: string
                                         type: string
+                                      templateAs:
+                                        default: Values
+                                        type: string
                                     required:
                                     required:
                                     - key
                                     - key
                                     type: object
                                     type: object
@@ -637,6 +643,9 @@ spec:
                               - items
                               - items
                               - name
                               - name
                               type: object
                               type: object
+                            target:
+                              default: Data
+                              type: string
                           type: object
                           type: object
                         type: array
                         type: array
                       type:
                       type:

+ 22 - 4
deploy/crds/bundle.yaml

@@ -307,8 +307,6 @@ spec:
                               type: object
                               type: object
                             templateFrom:
                             templateFrom:
                               items:
                               items:
-                                maxProperties: 1
-                                minProperties: 1
                                 properties:
                                 properties:
                                   configMap:
                                   configMap:
                                     properties:
                                     properties:
@@ -317,6 +315,9 @@ spec:
                                           properties:
                                           properties:
                                             key:
                                             key:
                                               type: string
                                               type: string
+                                            templateAs:
+                                              default: Values
+                                              type: string
                                           required:
                                           required:
                                             - key
                                             - key
                                           type: object
                                           type: object
@@ -327,6 +328,8 @@ spec:
                                       - items
                                       - items
                                       - name
                                       - name
                                     type: object
                                     type: object
+                                  literal:
+                                    type: string
                                   secret:
                                   secret:
                                     properties:
                                     properties:
                                       items:
                                       items:
@@ -334,6 +337,9 @@ spec:
                                           properties:
                                           properties:
                                             key:
                                             key:
                                               type: string
                                               type: string
+                                            templateAs:
+                                              default: Values
+                                              type: string
                                           required:
                                           required:
                                             - key
                                             - key
                                           type: object
                                           type: object
@@ -344,6 +350,9 @@ spec:
                                       - items
                                       - items
                                       - name
                                       - name
                                     type: object
                                     type: object
+                                  target:
+                                    default: Data
+                                    type: string
                                 type: object
                                 type: object
                               type: array
                               type: array
                             type:
                             type:
@@ -3418,8 +3427,6 @@ spec:
                           type: object
                           type: object
                         templateFrom:
                         templateFrom:
                           items:
                           items:
-                            maxProperties: 1
-                            minProperties: 1
                             properties:
                             properties:
                               configMap:
                               configMap:
                                 properties:
                                 properties:
@@ -3428,6 +3435,9 @@ spec:
                                       properties:
                                       properties:
                                         key:
                                         key:
                                           type: string
                                           type: string
+                                        templateAs:
+                                          default: Values
+                                          type: string
                                       required:
                                       required:
                                         - key
                                         - key
                                       type: object
                                       type: object
@@ -3438,6 +3448,8 @@ spec:
                                   - items
                                   - items
                                   - name
                                   - name
                                 type: object
                                 type: object
+                              literal:
+                                type: string
                               secret:
                               secret:
                                 properties:
                                 properties:
                                   items:
                                   items:
@@ -3445,6 +3457,9 @@ spec:
                                       properties:
                                       properties:
                                         key:
                                         key:
                                           type: string
                                           type: string
+                                        templateAs:
+                                          default: Values
+                                          type: string
                                       required:
                                       required:
                                         - key
                                         - key
                                       type: object
                                       type: object
@@ -3455,6 +3470,9 @@ spec:
                                   - items
                                   - items
                                   - name
                                   - name
                                 type: object
                                 type: object
+                              target:
+                                default: Data
+                                type: string
                             type: object
                             type: object
                           type: array
                           type: array
                         type:
                         type:

+ 80 - 0
docs/api/spec.md

@@ -4833,6 +4833,30 @@ TemplateRef
 <td>
 <td>
 </td>
 </td>
 </tr>
 </tr>
+<tr>
+<td>
+<code>target</code></br>
+<em>
+<a href="#external-secrets.io/v1beta1.TemplateTarget">
+TemplateTarget
+</a>
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+</td>
+</tr>
+<tr>
+<td>
+<code>literal</code></br>
+<em>
+string
+</em>
+</td>
+<td>
+<em>(Optional)</em>
+</td>
+</tr>
 </tbody>
 </tbody>
 </table>
 </table>
 <h3 id="external-secrets.io/v1beta1.TemplateRef">TemplateRef
 <h3 id="external-secrets.io/v1beta1.TemplateRef">TemplateRef
@@ -4901,8 +4925,64 @@ string
 <td>
 <td>
 </td>
 </td>
 </tr>
 </tr>
+<tr>
+<td>
+<code>templateAs</code></br>
+<em>
+<a href="#external-secrets.io/v1beta1.TemplateScope">
+TemplateScope
+</a>
+</em>
+</td>
+<td>
+</td>
+</tr>
 </tbody>
 </tbody>
 </table>
 </table>
+<h3 id="external-secrets.io/v1beta1.TemplateScope">TemplateScope
+(<code>string</code> alias)</p></h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1beta1.TemplateRefItem">TemplateRefItem</a>)
+</p>
+<p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Value</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody><tr><td><p>&#34;KeysAndValues&#34;</p></td>
+<td></td>
+</tr><tr><td><p>&#34;Values&#34;</p></td>
+<td></td>
+</tr></tbody>
+</table>
+<h3 id="external-secrets.io/v1beta1.TemplateTarget">TemplateTarget
+(<code>string</code> alias)</p></h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1beta1.TemplateFrom">TemplateFrom</a>)
+</p>
+<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/v1beta1.TokenAuth">TokenAuth
 <h3 id="external-secrets.io/v1beta1.TokenAuth">TokenAuth
 </h3>
 </h3>
 <p>
 <p>

+ 8 - 0
docs/guides/templating.md

@@ -24,6 +24,14 @@ You do not have to define your templates inline in an ExternalSecret but you can
 {% include 'template-v2-from-secret.yaml' %}
 {% include 'template-v2-from-secret.yaml' %}
 ```
 ```
 
 
+`TemplateFrom` also gives you the ability to Target your template to the Secret's Annotations, Labels or the Data block. It also allows you to render the templated information as `Values` or as `KeysAndValues` through the `templateAs` configuration:
+
+```yaml
+{% include 'template-v2-scope-and-target.yaml' %}
+```
+
+Lastly, `TemplateFrom` also supports adding `Literal` blocks for quick templating. These `Literal` blocks differ from `Template.Data` as they are rendered as a a `key:value` pair (while the `Template.Data`, you can only template the value).
+
 ### Extract Keys and Certificates from PKCS#12 Archive
 ### Extract Keys and Certificates from PKCS#12 Archive
 
 
 You can use pre-defined functions to extract data from your secrets. Here: extract keys and certificates from a PKCS#12 archive and store it as PEM.
 You can use pre-defined functions to extract data from your secrets. Here: extract keys and certificates from a PKCS#12 archive and store it as PEM.

+ 31 - 2
docs/snippets/template-v2-from-secret.yaml

@@ -10,9 +10,23 @@ data:
       - name: Graphite
       - name: Graphite
         type: graphite
         type: graphite
         access: proxy
         access: proxy
-        url: http://localhost:8080
+        url: "{{ .uri }}"
         password: "{{ .password }}"
         password: "{{ .password }}"
         user: "{{ .user }}"
         user: "{{ .user }}"
+  templated: |
+     # key and value templated
+     my-application-{{ .user}}: {{ .password | b64enc }}
+     # conditional keys
+     {{- if hasPrefix "oci://" .uri }}
+     enableOCI: true
+     {{- else }}
+     enableOCI: false
+     {{- end }}
+     # Fixed values
+     application-type: grafana
+  annotations: |
+     #dynamic timestamp generation
+     last-synced-for-user/{{ .user }}: {{ now }}
 ---
 ---
 apiVersion: external-secrets.io/v1beta1
 apiVersion: external-secrets.io/v1beta1
 kind: ExternalSecret
 kind: ExternalSecret
@@ -25,12 +39,24 @@ spec:
     template:
     template:
       engineVersion: v2
       engineVersion: v2
       templateFrom:
       templateFrom:
-      - configMap:
+      - target: Data
+        configMap:
           # name of the configmap to pull in
           # name of the configmap to pull in
           name: grafana-config-tpl
           name: grafana-config-tpl
           # here you define the keys that should be used as template
           # here you define the keys that should be used as template
           items:
           items:
           - key: config.yaml
           - key: config.yaml
+            templateAs: Values
+          - key: generated
+            templateAs: KeysAndValues
+      - target: Annotations
+        configMap:
+          # name of the configmap to pull in
+          name: grafana-config-tpl
+          # here you define the keys that should be used as template
+          items:
+          - key: annotations
+            templateAs: KeysAndValues
   data:
   data:
   - secretKey: user
   - secretKey: user
     remoteRef:
     remoteRef:
@@ -38,4 +64,7 @@ spec:
   - secretKey: password
   - secretKey: password
     remoteRef:
     remoteRef:
       key: /grafana/password
       key: /grafana/password
+  - secretKey: uri
+    remoteRef:
+      key: /grafana/uri
 {% endraw %}
 {% endraw %}

+ 22 - 0
docs/snippets/template-v2-scope-and-target.yaml

@@ -0,0 +1,22 @@
+{% raw %}
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+  name: my-template-example
+spec:
+  # ...
+  target:
+    name: secret-to-be-created
+    template:
+      engineVersion: v2
+      templateFrom:
+      - target: Annotations
+        literal: "last-sync-for-user/{{ .user }}: {{ .now }}"
+  data:
+  - secretKey: user
+    remoteRef:
+      key: /grafana/user
+  - secretKey: password
+    remoteRef:
+      key: /grafana/password
+{% endraw %}

File diff suppressed because it is too large
+ 6120 - 0
docs/spec.md


+ 11 - 5
e2e/framework/testcase.go

@@ -18,13 +18,13 @@ import (
 	"time"
 	"time"
 
 
 	//nolint
 	//nolint
-	. "github.com/onsi/gomega"
-	v1 "k8s.io/api/core/v1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-
 	"github.com/external-secrets/external-secrets-e2e/framework/log"
 	"github.com/external-secrets/external-secrets-e2e/framework/log"
 	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
 	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	. "github.com/onsi/gomega"
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
 )
 )
 
 
 var TargetSecretName = "target-secret"
 var TargetSecretName = "target-secret"
@@ -34,6 +34,7 @@ type TestCase struct {
 	Framework              *Framework
 	Framework              *Framework
 	ExternalSecret         *esv1beta1.ExternalSecret
 	ExternalSecret         *esv1beta1.ExternalSecret
 	ExternalSecretV1Alpha1 *esv1alpha1.ExternalSecret
 	ExternalSecretV1Alpha1 *esv1alpha1.ExternalSecret
+	AdditionalObjects      []client.Object
 	Secrets                map[string]SecretEntry
 	Secrets                map[string]SecretEntry
 	ExpectedSecret         *v1.Secret
 	ExpectedSecret         *v1.Secret
 	AfterSync              func(SecretStoreProvider, *v1.Secret)
 	AfterSync              func(SecretStoreProvider, *v1.Secret)
@@ -81,7 +82,12 @@ func TableFunc(f *Framework, prov SecretStoreProvider) func(...func(*TestCase))
 			err = tc.Framework.CRClient.Create(context.Background(), tc.ExternalSecret)
 			err = tc.Framework.CRClient.Create(context.Background(), tc.ExternalSecret)
 			Expect(err).ToNot(HaveOccurred())
 			Expect(err).ToNot(HaveOccurred())
 		}
 		}
-
+		if tc.AdditionalObjects != nil {
+			for _, obj := range tc.AdditionalObjects {
+				err = tc.Framework.CRClient.Create(context.Background(), obj)
+				Expect(err).ToNot(HaveOccurred())
+			}
+		}
 		// in case target name is empty
 		// in case target name is empty
 		if tc.ExternalSecret.Spec.Target.Name == "" {
 		if tc.ExternalSecret.Spec.Target.Name == "" {
 			TargetSecretName = tc.ExternalSecret.ObjectMeta.Name
 			TargetSecretName = tc.ExternalSecret.ObjectMeta.Name

+ 113 - 0
e2e/suites/provider/cases/common/common.go

@@ -21,6 +21,7 @@ import (
 	v1 "k8s.io/api/core/v1"
 	v1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
 
 
 	"github.com/external-secrets/external-secrets-e2e/framework"
 	"github.com/external-secrets/external-secrets-e2e/framework"
 	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
 	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
@@ -262,6 +263,118 @@ func JSONDataWithTemplate(f *framework.Framework) (string, func(*framework.TestC
 	}
 	}
 }
 }
 
 
+// This case creates multiple secrets with json values and renders a template from string.
+// The data is extracted from the JSON key using ref.Property.
+func JSONDataWithTemplateFromLiteral(f *framework.Framework) (string, func(*framework.TestCase)) {
+	return "[common] should sync json secrets with template", func(tc *framework.TestCase) {
+		secretKey1 := fmt.Sprintf("%s-%s", f.Namespace.Name, "one")
+		secretKey2 := fmt.Sprintf("%s-%s", f.Namespace.Name, "other")
+		tc.Secrets = map[string]framework.SecretEntry{
+			secretKey1: {Value: secretValue1},
+			secretKey2: {Value: secretValue2},
+		}
+		tc.ExpectedSecret = &v1.Secret{
+			Type: v1.SecretTypeOpaque,
+			Data: map[string][]byte{
+				"executed-foo1-val": []byte(`bar2-val`),
+			},
+		}
+		tplString := `executed-{{ .one }}: {{ .two }}`
+		tc.ExternalSecret.Spec.Target.Template = &esv1beta1.ExternalSecretTemplate{
+			TemplateFrom: []esv1beta1.TemplateFrom{
+				{
+					Literal: &tplString,
+					Target:  esv1beta1.TemplateTargetData,
+				},
+			},
+		}
+		tc.ExternalSecret.Spec.Data = []esv1beta1.ExternalSecretData{
+			{
+				SecretKey: "one",
+				RemoteRef: esv1beta1.ExternalSecretDataRemoteRef{
+					Key:      secretKey1,
+					Property: "foo1",
+				},
+			},
+			{
+				SecretKey: "two",
+				RemoteRef: esv1beta1.ExternalSecretDataRemoteRef{
+					Key:      secretKey2,
+					Property: "bar2",
+				},
+			},
+		}
+	}
+}
+
+// This case creates multiple secrets with json values and renders a template from string.
+// The data is extracted from the JSON key using ref.Property.
+func TemplateFromConfigmaps(f *framework.Framework) (string, func(*framework.TestCase)) {
+	return "[common] should sync from templateFrom Configmaps", func(tc *framework.TestCase) {
+		secretKey1 := fmt.Sprintf("%s-%s", f.Namespace.Name, "one")
+		secretKey2 := fmt.Sprintf("%s-%s", f.Namespace.Name, "other")
+		tc.AdditionalObjects = []client.Object{
+			&v1.ConfigMap{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "test",
+					Namespace: f.Namespace.Name,
+				},
+				Data: map[string]string{
+					"config.json": `executed-{{ .one}}-{{ .two}}`,
+					"templated":   `executed-{{ .one}}: {{ .two}}`,
+				},
+			},
+		}
+		tc.Secrets = map[string]framework.SecretEntry{
+			secretKey1: {Value: secretValue1},
+			secretKey2: {Value: secretValue2},
+		}
+		tc.ExpectedSecret = &v1.Secret{
+			Type: v1.SecretTypeOpaque,
+			Data: map[string][]byte{
+				"executed-foo1-val": []byte(`bar2-val`),
+				"config.json":       []byte(`executed-foo1-val-bar2-val`),
+			},
+		}
+		tc.ExternalSecret.Spec.Target.Template = &esv1beta1.ExternalSecretTemplate{
+			TemplateFrom: []esv1beta1.TemplateFrom{
+				{
+					ConfigMap: &esv1beta1.TemplateRef{
+						Name: "test",
+						Items: []esv1beta1.TemplateRefItem{
+							{
+								Key:        "config.json",
+								TemplateAs: esv1beta1.TemplateScopeValues,
+							},
+							{
+								Key:        "templated",
+								TemplateAs: esv1beta1.TemplateScopeKeysAndValues,
+							},
+						},
+					},
+					Target: esv1beta1.TemplateTargetData,
+				},
+			},
+		}
+		tc.ExternalSecret.Spec.Data = []esv1beta1.ExternalSecretData{
+			{
+				SecretKey: "one",
+				RemoteRef: esv1beta1.ExternalSecretDataRemoteRef{
+					Key:      secretKey1,
+					Property: "foo1",
+				},
+			},
+			{
+				SecretKey: "two",
+				RemoteRef: esv1beta1.ExternalSecretDataRemoteRef{
+					Key:      secretKey2,
+					Property: "bar2",
+				},
+			},
+		}
+	}
+}
+
 // This case creates one secret with json values and syncs them using a single .Spec.DataFrom block.
 // This case creates one secret with json values and syncs them using a single .Spec.DataFrom block.
 func JSONDataFromSync(f *framework.Framework) (string, func(*framework.TestCase)) {
 func JSONDataFromSync(f *framework.Framework) (string, func(*framework.TestCase)) {
 	return "[common] should sync secrets with dataFrom", func(tc *framework.TestCase) {
 	return "[common] should sync secrets with dataFrom", func(tc *framework.TestCase) {

+ 2 - 0
e2e/suites/provider/cases/vault/vault.go

@@ -52,6 +52,8 @@ var _ = Describe("[vault]", Label("vault"), func() {
 		framework.Compose(withTokenAuth, f, common.JSONDataWithoutTargetName, useTokenAuth),
 		framework.Compose(withTokenAuth, f, common.JSONDataWithoutTargetName, useTokenAuth),
 		framework.Compose(withTokenAuth, f, common.SyncV1Alpha1, useTokenAuth),
 		framework.Compose(withTokenAuth, f, common.SyncV1Alpha1, useTokenAuth),
 		framework.Compose(withTokenAuth, f, common.DecodingPolicySync, useTokenAuth),
 		framework.Compose(withTokenAuth, f, common.DecodingPolicySync, useTokenAuth),
+		framework.Compose(withTokenAuth, f, common.JSONDataWithTemplateFromLiteral, useTokenAuth),
+		framework.Compose(withTokenAuth, f, common.TemplateFromConfigmaps, useTokenAuth),
 		// use cert auth
 		// use cert auth
 		framework.Compose(withCertAuth, f, common.FindByName, useCertAuth),
 		framework.Compose(withCertAuth, f, common.FindByName, useCertAuth),
 		framework.Compose(withCertAuth, f, common.FindByNameAndRewrite, useCertAuth),
 		framework.Compose(withCertAuth, f, common.FindByNameAndRewrite, useCertAuth),

+ 132 - 91
pkg/controllers/externalsecret/externalsecret_controller_template.go

@@ -29,6 +29,119 @@ import (
 	utils "github.com/external-secrets/external-secrets/pkg/utils"
 	utils "github.com/external-secrets/external-secrets/pkg/utils"
 )
 )
 
 
+type Parser struct {
+	exec         template.ExecFunc
+	dataMap      map[string][]byte
+	client       client.Client
+	targetSecret *v1.Secret
+}
+
+func (p *Parser) MergeConfigMap(ctx context.Context, namespace string, tpl esv1beta1.TemplateFrom) error {
+	if tpl.ConfigMap == nil {
+		return nil
+	}
+	var cm v1.ConfigMap
+	err := p.client.Get(ctx, types.NamespacedName{
+		Name:      tpl.ConfigMap.Name,
+		Namespace: namespace,
+	}, &cm)
+	if err != nil {
+		return err
+	}
+	for _, k := range tpl.ConfigMap.Items {
+		val, ok := cm.Data[k.Key]
+		out := make(map[string][]byte)
+		if !ok {
+			return fmt.Errorf(errTplCMMissingKey, tpl.ConfigMap.Name, k.Key)
+		}
+		switch k.TemplateAs {
+		case esv1beta1.TemplateScopeValues:
+			out[k.Key] = []byte(val)
+		case esv1beta1.TemplateScopeKeysAndValues:
+			out[val] = []byte(val)
+		}
+		err = p.exec(out, p.dataMap, k.TemplateAs, tpl.Target, p.targetSecret)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (p *Parser) MergeSecret(ctx context.Context, namespace string, tpl esv1beta1.TemplateFrom) error {
+	if tpl.Secret == nil {
+		return nil
+	}
+	var sec v1.Secret
+	err := p.client.Get(ctx, types.NamespacedName{
+		Name:      tpl.Secret.Name,
+		Namespace: namespace,
+	}, &sec)
+	if err != nil {
+		return err
+	}
+	for _, k := range tpl.Secret.Items {
+		val, ok := sec.Data[k.Key]
+		if !ok {
+			return fmt.Errorf(errTplSecMissingKey, tpl.Secret.Name, k.Key)
+		}
+		out := make(map[string][]byte)
+		switch k.TemplateAs {
+		case esv1beta1.TemplateScopeValues:
+			out[k.Key] = val
+		case esv1beta1.TemplateScopeKeysAndValues:
+			out[string(val)] = val
+		}
+		err = p.exec(out, p.dataMap, k.TemplateAs, tpl.Target, p.targetSecret)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (p *Parser) MergeLiteral(ctx context.Context, tpl esv1beta1.TemplateFrom) error {
+	if tpl.Literal == nil {
+		return nil
+	}
+	out := make(map[string][]byte)
+	out[*tpl.Literal] = []byte(*tpl.Literal)
+	return p.exec(out, p.dataMap, esv1beta1.TemplateScopeKeysAndValues, tpl.Target, p.targetSecret)
+}
+
+func (p *Parser) MergeTemplateFrom(ctx context.Context, es *esv1beta1.ExternalSecret) error {
+	if es.Spec.Target.Template == nil {
+		return nil
+	}
+	for _, tpl := range es.Spec.Target.Template.TemplateFrom {
+		err := p.MergeConfigMap(ctx, es.Namespace, tpl)
+		if err != nil {
+			return err
+		}
+		err = p.MergeSecret(ctx, es.Namespace, tpl)
+		if err != nil {
+			return err
+		}
+		err = p.MergeLiteral(ctx, tpl)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (p *Parser) MergeMap(tplMap map[string]string, target esv1beta1.TemplateTarget) error {
+	byteMap := make(map[string][]byte)
+	for k, v := range tplMap {
+		byteMap[k] = []byte(v)
+	}
+	err := p.exec(byteMap, p.dataMap, esv1beta1.TemplateScopeValues, target, p.targetSecret)
+	if err != nil {
+		return fmt.Errorf(errExecTpl, err)
+	}
+	return nil
+}
+
 // merge template in the following order:
 // merge template in the following order:
 // * template.Data (highest precedence)
 // * template.Data (highest precedence)
 // * template.templateFrom
 // * template.templateFrom
@@ -42,47 +155,38 @@ func (r *Reconciler) applyTemplate(ctx context.Context, es *esv1beta1.ExternalSe
 		secret.Annotations[esv1beta1.AnnotationDataHash] = utils.ObjectHash(secret.Data)
 		secret.Annotations[esv1beta1.AnnotationDataHash] = utils.ObjectHash(secret.Data)
 		return nil
 		return nil
 	}
 	}
+	execute, err := template.EngineForVersion(es.Spec.Target.Template.EngineVersion)
+	if err != nil {
+		return err
+	}
 
 
-	// fetch templates defined in template.templateFrom
-	tplMap, err := r.getTemplateData(ctx, es)
+	p := Parser{
+		client:       r.Client,
+		targetSecret: secret,
+		dataMap:      dataMap,
+		exec:         execute,
+	}
+	// apply templates defined in template.templateFrom
+	err = p.MergeTemplateFrom(ctx, es)
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf(errFetchTplFrom, err)
 		return fmt.Errorf(errFetchTplFrom, err)
 	}
 	}
-
 	// explicitly defined template.Data takes precedence over templateFrom
 	// explicitly defined template.Data takes precedence over templateFrom
-	for k, v := range es.Spec.Target.Template.Data {
-		tplMap[k] = []byte(v)
+	err = p.MergeMap(es.Spec.Target.Template.Data, esv1beta1.TemplateTargetData)
+	if err != nil {
+		return fmt.Errorf(errExecTpl, err)
 	}
 	}
-	r.Log.V(1).Info("found template data", "tpl_data", tplMap)
-
-	tplMapLabels := make(map[string][]byte)
-	tplMapAnnotations := make(map[string][]byte)
 
 
 	// get template data for labels
 	// get template data for labels
-	if es.Spec.Target.Template.Metadata.Labels != nil {
-		for k, v := range es.Spec.Target.Template.Metadata.Labels {
-			tplMapLabels[k] = []byte(v)
-		}
-		r.Log.V(1).Info("found template metadata (labels)", "tpl_labels", tplMapLabels)
-	}
-
-	// get template data for annotations
-	if es.Spec.Target.Template.Metadata.Annotations != nil {
-		for k, v := range es.Spec.Target.Template.Metadata.Annotations {
-			tplMapAnnotations[k] = []byte(v)
-		}
-		r.Log.V(1).Info("found template metadata (annotations)", "tpl_annotations", tplMapAnnotations)
-	}
-
-	execute, err := template.EngineForVersion(es.Spec.Target.Template.EngineVersion)
+	err = p.MergeMap(es.Spec.Target.Template.Metadata.Labels, esv1beta1.TemplateTargetLabels)
 	if err != nil {
 	if err != nil {
-		return err
+		return fmt.Errorf(errExecTpl, err)
 	}
 	}
-	err = execute(tplMap, tplMapLabels, tplMapAnnotations, dataMap, secret)
+	// get template data for labels
+	err = p.MergeMap(es.Spec.Target.Template.Metadata.Annotations, esv1beta1.TemplateTargetAnnotations)
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf(errExecTpl, err)
 		return fmt.Errorf(errExecTpl, err)
 	}
 	}
-
 	// if no data was provided by template fallback
 	// if no data was provided by template fallback
 	// to value from the provider
 	// to value from the provider
 	if len(es.Spec.Target.Template.Data) == 0 && len(es.Spec.Target.Template.TemplateFrom) == 0 {
 	if len(es.Spec.Target.Template.Data) == 0 && len(es.Spec.Target.Template.TemplateFrom) == 0 {
@@ -112,66 +216,3 @@ func mergeMetadata(secret *v1.Secret, externalSecret *esv1beta1.ExternalSecret)
 	utils.MergeStringMap(secret.ObjectMeta.Labels, externalSecret.Spec.Target.Template.Metadata.Labels)
 	utils.MergeStringMap(secret.ObjectMeta.Labels, externalSecret.Spec.Target.Template.Metadata.Labels)
 	utils.MergeStringMap(secret.ObjectMeta.Annotations, externalSecret.Spec.Target.Template.Metadata.Annotations)
 	utils.MergeStringMap(secret.ObjectMeta.Annotations, externalSecret.Spec.Target.Template.Metadata.Annotations)
 }
 }
-
-func (r *Reconciler) getTemplateData(ctx context.Context, externalSecret *esv1beta1.ExternalSecret) (map[string][]byte, error) {
-	out := make(map[string][]byte)
-	if externalSecret.Spec.Target.Template == nil {
-		return out, nil
-	}
-	for _, tpl := range externalSecret.Spec.Target.Template.TemplateFrom {
-		err := mergeConfigMap(ctx, r.Client, externalSecret, tpl, out)
-		if err != nil {
-			return nil, err
-		}
-		err = mergeSecret(ctx, r.Client, externalSecret, tpl, out)
-		if err != nil {
-			return nil, err
-		}
-	}
-	return out, nil
-}
-
-func mergeConfigMap(ctx context.Context, k8sClient client.Client, es *esv1beta1.ExternalSecret, tpl esv1beta1.TemplateFrom, out map[string][]byte) error {
-	if tpl.ConfigMap == nil {
-		return nil
-	}
-
-	var cm v1.ConfigMap
-	err := k8sClient.Get(ctx, types.NamespacedName{
-		Name:      tpl.ConfigMap.Name,
-		Namespace: es.Namespace,
-	}, &cm)
-	if err != nil {
-		return err
-	}
-	for _, k := range tpl.ConfigMap.Items {
-		val, ok := cm.Data[k.Key]
-		if !ok {
-			return fmt.Errorf(errTplCMMissingKey, tpl.ConfigMap.Name, k.Key)
-		}
-		out[k.Key] = []byte(val)
-	}
-	return nil
-}
-
-func mergeSecret(ctx context.Context, k8sClient client.Client, es *esv1beta1.ExternalSecret, tpl esv1beta1.TemplateFrom, out map[string][]byte) error {
-	if tpl.Secret == nil {
-		return nil
-	}
-	var sec v1.Secret
-	err := k8sClient.Get(ctx, types.NamespacedName{
-		Name:      tpl.Secret.Name,
-		Namespace: es.Namespace,
-	}, &sec)
-	if err != nil {
-		return err
-	}
-	for _, k := range tpl.Secret.Items {
-		val, ok := sec.Data[k.Key]
-		if !ok {
-			return fmt.Errorf(errTplSecMissingKey, tpl.Secret.Name, k.Key)
-		}
-		out[k.Key] = val
-	}
-	return nil
-}

+ 129 - 6
pkg/controllers/externalsecret/externalsecret_controller_test.go

@@ -617,12 +617,12 @@ var _ = Describe("ExternalSecret controller", func() {
 			Expect(string(secret.Data[targetProp])).To(Equal(expectedSecretVal))
 			Expect(string(secret.Data[targetProp])).To(Equal(expectedSecretVal))
 		}
 		}
 	}
 	}
-
-	// secret should be synced with correct value precedence:
-	// * template
-	// * templateFrom
-	// * data
-	// * dataFrom
+	// // secret should be synced with correct value precedence:
+	// // * fromString
+	// // * template data
+	// // * templateFrom
+	// // * data
+	// // * dataFrom
 	syncWithTemplatePrecedence := func(tc *testCase) {
 	syncWithTemplatePrecedence := func(tc *testCase) {
 		const secretVal = "someValue"
 		const secretVal = "someValue"
 		const tplStaticKey = "tplstatickey"
 		const tplStaticKey = "tplstatickey"
@@ -706,6 +706,127 @@ var _ = Describe("ExternalSecret controller", func() {
 			Expect(string(secret.Data[tplFromSecKey])).To(Equal("tpl-from-sec-value: someValue // map-bar-value"))
 			Expect(string(secret.Data[tplFromSecKey])).To(Equal("tpl-from-sec-value: someValue // map-bar-value"))
 		}
 		}
 	}
 	}
+	syncTemplateFromKeysAndValues := func(tc *testCase) {
+		const tplFromCMName = "template-cm"
+		const tplFromSecretName = "template-secret"
+		const tplFromKey = "tpl-from-key"
+		const tplFromSecKey = "tpl-from-sec-key"
+		const tplFromVal = "{{ .targetKey }}-cm: {{ .targetValue }}"
+		const tplFromSecVal = "{{ .targetKey }}-sec: {{ .targetValue }}"
+		Expect(k8sClient.Create(context.Background(), &v1.ConfigMap{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      tplFromCMName,
+				Namespace: ExternalSecretNamespace,
+			},
+			Data: map[string]string{
+				tplFromKey: tplFromVal,
+			},
+		})).To(Succeed())
+		Expect(k8sClient.Create(context.Background(), &v1.Secret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      tplFromSecretName,
+				Namespace: ExternalSecretNamespace,
+			},
+			Data: map[string][]byte{
+				tplFromSecKey: []byte(tplFromSecVal),
+			},
+		})).To(Succeed())
+		tc.externalSecret.Spec.Target.Template = &esv1beta1.ExternalSecretTemplate{
+			Metadata: esv1beta1.ExternalSecretTemplateMetadata{},
+			Type:     v1.SecretTypeOpaque,
+			TemplateFrom: []esv1beta1.TemplateFrom{
+				{
+					ConfigMap: &esv1beta1.TemplateRef{
+						Name: tplFromCMName,
+						Items: []esv1beta1.TemplateRefItem{
+							{
+								Key:        tplFromKey,
+								TemplateAs: esv1beta1.TemplateScopeKeysAndValues,
+							},
+						},
+					},
+				},
+				{
+					Secret: &esv1beta1.TemplateRef{
+						Name: tplFromSecretName,
+						Items: []esv1beta1.TemplateRefItem{
+							{
+								Key:        tplFromSecKey,
+								TemplateAs: esv1beta1.TemplateScopeKeysAndValues,
+							},
+						},
+					},
+				},
+			},
+		}
+		tc.externalSecret.Spec.DataFrom = []esv1beta1.ExternalSecretDataFromRemoteRef{
+			{
+				Extract: &esv1beta1.ExternalSecretDataRemoteRef{
+					Key: "datamap",
+				},
+			},
+		}
+		fakeProvider.WithGetSecretMap(map[string][]byte{
+			"targetKey":   []byte(FooValue),
+			"targetValue": []byte(BarValue),
+		}, nil)
+		tc.checkSecret = func(es *esv1beta1.ExternalSecret, secret *v1.Secret) {
+			// check values
+			Expect(string(secret.Data["map-foo-value-cm"])).To(Equal(BarValue))
+			Expect(string(secret.Data["map-foo-value-sec"])).To(Equal(BarValue))
+		}
+	}
+	syncTemplateFromLiteral := func(tc *testCase) {
+		tplDataVal := "{{ .targetKey }}-literal: {{ .targetValue }}"
+		tplAnnotationsVal := "{{ .targetKey }}-annotations: {{ .targetValue }}"
+		tplLabelsVal := "{{ .targetKey }}-labels: {{ .targetValue }}"
+		tplComplexVal := `
+{{- range $k, $v := ( .complex | fromJson )}}
+{{ $k }}: {{ $v }}
+{{- end }}
+`
+		tc.externalSecret.Spec.Target.Template = &esv1beta1.ExternalSecretTemplate{
+			Metadata: esv1beta1.ExternalSecretTemplateMetadata{},
+			Type:     v1.SecretTypeOpaque,
+			TemplateFrom: []esv1beta1.TemplateFrom{
+				{
+					Literal: &tplDataVal,
+				},
+				{
+					Literal: &tplComplexVal,
+				},
+				{
+					Target:  esv1beta1.TemplateTargetAnnotations,
+					Literal: &tplAnnotationsVal,
+				},
+				{
+					Target:  esv1beta1.TemplateTargetLabels,
+					Literal: &tplLabelsVal,
+				},
+			},
+		}
+		tc.externalSecret.Spec.DataFrom = []esv1beta1.ExternalSecretDataFromRemoteRef{
+			{
+				Extract: &esv1beta1.ExternalSecretDataRemoteRef{
+					Key: "datamap",
+				},
+			},
+		}
+		fakeProvider.WithGetSecretMap(map[string][]byte{
+			"targetKey":   []byte(FooValue),
+			"targetValue": []byte(BarValue),
+			"complex":     []byte("{\"nested\":\"json\",\"can\":\"be\",\"templated\":\"successfully\"}"),
+		}, nil)
+		tc.checkSecret = func(es *esv1beta1.ExternalSecret, secret *v1.Secret) {
+			// check values
+			Expect(string(secret.Data["map-foo-value-literal"])).To(Equal(BarValue))
+			Expect(string(secret.Data["nested"])).To(Equal("json"))
+			Expect(string(secret.Data["can"])).To(Equal("be"))
+			Expect(string(secret.Data["templated"])).To(Equal("successfully"))
+			Expect(secret.ObjectMeta.Annotations["map-foo-value-annotations"]).To(Equal(BarValue))
+			Expect(secret.ObjectMeta.Labels["map-foo-value-labels"]).To(Equal(BarValue))
+		}
+	}
 
 
 	refreshWithTemplate := func(tc *testCase) {
 	refreshWithTemplate := func(tc *testCase) {
 		const secretVal = "someValue"
 		const secretVal = "someValue"
@@ -1794,6 +1915,8 @@ var _ = Describe("ExternalSecret controller", func() {
 		Entry("should sync with template", syncWithTemplate),
 		Entry("should sync with template", syncWithTemplate),
 		Entry("should sync with template engine v2", syncWithTemplateV2),
 		Entry("should sync with template engine v2", syncWithTemplateV2),
 		Entry("should sync template with correct value precedence", syncWithTemplatePrecedence),
 		Entry("should sync template with correct value precedence", syncWithTemplatePrecedence),
+		Entry("should sync template from keys and values", syncTemplateFromKeysAndValues),
+		Entry("should sync template from literal", syncTemplateFromLiteral),
 		Entry("should refresh secret from template", refreshWithTemplate),
 		Entry("should refresh secret from template", refreshWithTemplate),
 		Entry("should be able to use only metadata from template", onlyMetadataFromTemplate),
 		Entry("should be able to use only metadata from template", onlyMetadataFromTemplate),
 		Entry("should refresh secret value when provider secret changes", refreshSecretValue),
 		Entry("should refresh secret value when provider secret changes", refreshSecretValue),

+ 1 - 1
pkg/template/engine.go

@@ -20,7 +20,7 @@ import (
 	v2 "github.com/external-secrets/external-secrets/pkg/template/v2"
 	v2 "github.com/external-secrets/external-secrets/pkg/template/v2"
 )
 )
 
 
-type ExecFunc func(tpl, labelsTpl, annotationsTpl, data map[string][]byte, secret *corev1.Secret) error
+type ExecFunc func(tpl, data map[string][]byte, scope esapi.TemplateScope, target esapi.TemplateTarget, secret *corev1.Secret) error
 
 
 func EngineForVersion(version esapi.TemplateEngineVersion) (ExecFunc, error) {
 func EngineForVersion(version esapi.TemplateEngineVersion) (ExecFunc, error) {
 	switch version {
 	switch version {

+ 3 - 1
pkg/template/v1/template.go

@@ -27,6 +27,8 @@ import (
 	"github.com/youmark/pkcs8"
 	"github.com/youmark/pkcs8"
 	"golang.org/x/crypto/pkcs12"
 	"golang.org/x/crypto/pkcs12"
 	corev1 "k8s.io/api/core/v1"
 	corev1 "k8s.io/api/core/v1"
+
+	esapi "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 )
 )
 
 
 var tplFuncs = tpl.FuncMap{
 var tplFuncs = tpl.FuncMap{
@@ -70,7 +72,7 @@ const (
 )
 )
 
 
 // Execute renders the secret data as template. If an error occurs processing is stopped immediately.
 // Execute renders the secret data as template. If an error occurs processing is stopped immediately.
-func Execute(tpl, labelsTpl, annotationsTpl, data map[string][]byte, secret *corev1.Secret) error {
+func Execute(tpl, data map[string][]byte, scope esapi.TemplateScope, target esapi.TemplateTarget, secret *corev1.Secret) error {
 	if tpl == nil {
 	if tpl == nil {
 		return nil
 		return nil
 	}
 	}

+ 1 - 1
pkg/template/v1/template_test.go

@@ -349,7 +349,7 @@ func TestExecute(t *testing.T) {
 				Data:       make(map[string][]byte),
 				Data:       make(map[string][]byte),
 				ObjectMeta: v1.ObjectMeta{Labels: make(map[string]string), Annotations: make(map[string]string)},
 				ObjectMeta: v1.ObjectMeta{Labels: make(map[string]string), Annotations: make(map[string]string)},
 			}
 			}
-			err := Execute(row.tpl, nil, nil, row.data, sec)
+			err := Execute(row.tpl, row.data, "", "", sec)
 			if !ErrorContains(err, row.expErr) {
 			if !ErrorContains(err, row.expErr) {
 				t.Errorf("unexpected error: %s, expected: %s", err, row.expErr)
 				t.Errorf("unexpected error: %s, expected: %s", err, row.expErr)
 			}
 			}

+ 51 - 16
pkg/template/v2/template.go

@@ -20,6 +20,9 @@ import (
 
 
 	"github.com/Masterminds/sprig/v3"
 	"github.com/Masterminds/sprig/v3"
 	corev1 "k8s.io/api/core/v1"
 	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/util/yaml"
+
+	esapi "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 )
 )
 
 
 var tplFuncs = tpl.FuncMap{
 var tplFuncs = tpl.FuncMap{
@@ -62,33 +65,65 @@ func init() {
 	}
 	}
 }
 }
 
 
-// Execute renders the secret data as template. If an error occurs processing is stopped immediately.
-func Execute(tpl, labelsTpl, annotationsTpl, data map[string][]byte, secret *corev1.Secret) error {
-	if tpl == nil && labelsTpl == nil && annotationsTpl == nil {
-		return nil
+func applyToTarget(k, val string, target esapi.TemplateTarget, secret *corev1.Secret) {
+	switch target {
+	case esapi.TemplateTargetAnnotations:
+		secret.Annotations[k] = val
+	case esapi.TemplateTargetLabels:
+		secret.Labels[k] = val
+	case esapi.TemplateTargetData:
+		secret.Data[k] = []byte(val)
+	default:
 	}
 	}
-	for k, v := range tpl {
+}
+
+func valueScopeApply(tplMap, data map[string][]byte, target esapi.TemplateTarget, secret *corev1.Secret) error {
+	for k, v := range tplMap {
 		val, err := execute(k, string(v), data)
 		val, err := execute(k, string(v), data)
 		if err != nil {
 		if err != nil {
 			return fmt.Errorf(errExecute, k, err)
 			return fmt.Errorf(errExecute, k, err)
 		}
 		}
-		secret.Data[k] = val
+		applyToTarget(k, string(val), target, secret)
 	}
 	}
+	return nil
+}
 
 
-	for k, v := range labelsTpl {
-		val, err := execute(k, string(v), data)
-		if err != nil {
-			return fmt.Errorf(errExecute, k, err)
-		}
-		secret.ObjectMeta.Labels[k] = string(val)
+func mapScopeApply(tpl string, data map[string][]byte, target esapi.TemplateTarget, secret *corev1.Secret) 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)
+	}
+	for k, val := range src {
+		applyToTarget(k, val, target, secret)
+	}
+	return nil
+}
 
 
-	for k, v := range annotationsTpl {
-		val, err := execute(k, string(v), data)
+// 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 {
+	if tpl == nil {
+		return nil
+	}
+	switch scope {
+	case esapi.TemplateScopeKeysAndValues:
+		for _, v := range tpl {
+			err := mapScopeApply(string(v), data, target, secret)
+			if err != nil {
+				return err
+			}
+		}
+	case esapi.TemplateScopeValues:
+		err := valueScopeApply(tpl, data, target, secret)
 		if err != nil {
 		if err != nil {
-			return fmt.Errorf(errExecute, k, err)
+			return err
 		}
 		}
-		secret.ObjectMeta.Annotations[k] = string(val)
+	default:
+		return fmt.Errorf("unknown scope '%v': expected 'Values' or 'KeysAndValues'", scope)
 	}
 	}
 	return nil
 	return nil
 }
 }

+ 145 - 29
pkg/template/v2/template_test.go

@@ -20,8 +20,11 @@ import (
 
 
 	"github.com/google/go-cmp/cmp"
 	"github.com/google/go-cmp/cmp"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 	corev1 "k8s.io/api/core/v1"
 	corev1 "k8s.io/api/core/v1"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	esapi "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 )
 )
 
 
 const (
 const (
@@ -140,11 +143,16 @@ func TestExecute(t *testing.T) {
 		tpl                 map[string][]byte
 		tpl                 map[string][]byte
 		labelsTpl           map[string][]byte
 		labelsTpl           map[string][]byte
 		annotationsTpl      map[string][]byte
 		annotationsTpl      map[string][]byte
+		stringDataTpl       map[string][]byte
 		data                map[string][]byte
 		data                map[string][]byte
-		expetedData         map[string][]byte
+		expectedData        map[string][]byte
+		expectedStringData  map[string]string
 		expectedLabels      map[string]string
 		expectedLabels      map[string]string
 		expectedAnnotations map[string]string
 		expectedAnnotations map[string]string
 		expErr              string
 		expErr              string
+		expLblErr           string
+		expAnnoErr          string
+		expStrErr           string
 	}{
 	}{
 		{
 		{
 			name:           "test empty",
 			name:           "test empty",
@@ -161,7 +169,7 @@ func TestExecute(t *testing.T) {
 			data: map[string][]byte{
 			data: map[string][]byte{
 				"secret": []byte("MTIzNA=="),
 				"secret": []byte("MTIzNA=="),
 			},
 			},
-			expetedData: map[string][]byte{
+			expectedData: map[string][]byte{
 				"foo": []byte("1234"),
 				"foo": []byte("1234"),
 			},
 			},
 		},
 		},
@@ -173,7 +181,7 @@ func TestExecute(t *testing.T) {
 			data: map[string][]byte{
 			data: map[string][]byte{
 				"secret": []byte(`{"foo": "bar"}`),
 				"secret": []byte(`{"foo": "bar"}`),
 			},
 			},
-			expetedData: map[string][]byte{
+			expectedData: map[string][]byte{
 				"foo": []byte("bar"),
 				"foo": []byte("bar"),
 			},
 			},
 		},
 		},
@@ -185,7 +193,7 @@ func TestExecute(t *testing.T) {
 			data: map[string][]byte{
 			data: map[string][]byte{
 				"secret": []byte(`{"foo": {"baz":"bang"}}`),
 				"secret": []byte(`{"foo": {"baz":"bang"}}`),
 			},
 			},
-			expetedData: map[string][]byte{
+			expectedData: map[string][]byte{
 				"foo": []byte(`{"baz":"bang"}`),
 				"foo": []byte(`{"baz":"bang"}`),
 			},
 			},
 		},
 		},
@@ -197,7 +205,7 @@ func TestExecute(t *testing.T) {
 			data: map[string][]byte{
 			data: map[string][]byte{
 				"secret": []byte(`{"foo": "bar"}`),
 				"secret": []byte(`{"foo": "bar"}`),
 			},
 			},
-			expetedData: map[string][]byte{
+			expectedData: map[string][]byte{
 				"foo": []byte(`foo: bar`),
 				"foo": []byte(`foo: bar`),
 			},
 			},
 		},
 		},
@@ -209,7 +217,7 @@ func TestExecute(t *testing.T) {
 			data: map[string][]byte{
 			data: map[string][]byte{
 				"secret": []byte(`foo: bar`),
 				"secret": []byte(`foo: bar`),
 			},
 			},
-			expetedData: map[string][]byte{
+			expectedData: map[string][]byte{
 				"foo": []byte(`{"foo":"bar"}`),
 				"foo": []byte(`{"foo":"bar"}`),
 			},
 			},
 		},
 		},
@@ -221,7 +229,7 @@ func TestExecute(t *testing.T) {
 			data: map[string][]byte{
 			data: map[string][]byte{
 				"path": []byte(`foo/bar/baz.exe`),
 				"path": []byte(`foo/bar/baz.exe`),
 			},
 			},
-			expetedData: map[string][]byte{
+			expectedData: map[string][]byte{
 				"foo": []byte(`.exe`),
 				"foo": []byte(`.exe`),
 			},
 			},
 		},
 		},
@@ -233,7 +241,7 @@ func TestExecute(t *testing.T) {
 			data: map[string][]byte{
 			data: map[string][]byte{
 				"conn": []byte(`postgres://user:pass@db.host:5432/dbname`),
 				"conn": []byte(`postgres://user:pass@db.host:5432/dbname`),
 			},
 			},
-			expetedData: map[string][]byte{
+			expectedData: map[string][]byte{
 				"foo": []byte(`db+postgresql://user:pass@db.host:5432/dbname`),
 				"foo": []byte(`db+postgresql://user:pass@db.host:5432/dbname`),
 			},
 			},
 		},
 		},
@@ -253,7 +261,7 @@ func TestExecute(t *testing.T) {
 				"user":     []byte(`foobert`),
 				"user":     []byte(`foobert`),
 				"password": []byte("harharhar"),
 				"password": []byte("harharhar"),
 			},
 			},
-			expetedData: map[string][]byte{
+			expectedData: map[string][]byte{
 				"cfg": []byte(`
 				"cfg": []byte(`
 		datasources:
 		datasources:
 		- name: Graphite
 		- name: Graphite
@@ -270,7 +278,7 @@ func TestExecute(t *testing.T) {
 				"foo": []byte(`{{ "123412341234" | b64enc | b64dec }}`),
 				"foo": []byte(`{{ "123412341234" | b64enc | b64dec }}`),
 			},
 			},
 			data: map[string][]byte{},
 			data: map[string][]byte{},
-			expetedData: map[string][]byte{
+			expectedData: map[string][]byte{
 				"foo": []byte("123412341234"),
 				"foo": []byte("123412341234"),
 			},
 			},
 		},
 		},
@@ -283,7 +291,7 @@ func TestExecute(t *testing.T) {
 			data: map[string][]byte{
 			data: map[string][]byte{
 				"secret": []byte(pkcs12ContentNoPass),
 				"secret": []byte(pkcs12ContentNoPass),
 			},
 			},
-			expetedData: map[string][]byte{
+			expectedData: map[string][]byte{
 				"key":  []byte(pkcs12Key),
 				"key":  []byte(pkcs12Key),
 				"cert": []byte(pkcs12Cert),
 				"cert": []byte(pkcs12Cert),
 			},
 			},
@@ -297,7 +305,7 @@ func TestExecute(t *testing.T) {
 			data: map[string][]byte{
 			data: map[string][]byte{
 				"secret": []byte(pkcs12ContentWithPass),
 				"secret": []byte(pkcs12ContentWithPass),
 			},
 			},
-			expetedData: map[string][]byte{
+			expectedData: map[string][]byte{
 				"key":  []byte(pkcs12Key),
 				"key":  []byte(pkcs12Key),
 				"cert": []byte(pkcs12Cert),
 				"cert": []byte(pkcs12Cert),
 			},
 			},
@@ -356,7 +364,7 @@ func TestExecute(t *testing.T) {
 			data: map[string][]byte{
 			data: map[string][]byte{
 				"secret": []byte(jwkPubRSA),
 				"secret": []byte(jwkPubRSA),
 			},
 			},
-			expetedData: map[string][]byte{
+			expectedData: map[string][]byte{
 				"fn": []byte(jwkPubRSAPKIX),
 				"fn": []byte(jwkPubRSAPKIX),
 			},
 			},
 		},
 		},
@@ -368,7 +376,7 @@ func TestExecute(t *testing.T) {
 			data: map[string][]byte{
 			data: map[string][]byte{
 				"secret": []byte(jwkPrivRSA),
 				"secret": []byte(jwkPrivRSA),
 			},
 			},
-			expetedData: map[string][]byte{
+			expectedData: map[string][]byte{
 				"fn": []byte(jwkPrivRSAPKCS8),
 				"fn": []byte(jwkPrivRSAPKCS8),
 			},
 			},
 		},
 		},
@@ -380,7 +388,7 @@ func TestExecute(t *testing.T) {
 			data: map[string][]byte{
 			data: map[string][]byte{
 				"secret": []byte(jwkPubEC),
 				"secret": []byte(jwkPubEC),
 			},
 			},
-			expetedData: map[string][]byte{
+			expectedData: map[string][]byte{
 				"fn": []byte(jwkPubECPKIX),
 				"fn": []byte(jwkPubECPKIX),
 			},
 			},
 		},
 		},
@@ -392,7 +400,7 @@ func TestExecute(t *testing.T) {
 			data: map[string][]byte{
 			data: map[string][]byte{
 				"secret": []byte(jwkPrivEC),
 				"secret": []byte(jwkPrivEC),
 			},
 			},
-			expetedData: map[string][]byte{
+			expectedData: map[string][]byte{
 				"fn": []byte(jwkPrivECPKCS8),
 				"fn": []byte(jwkPrivECPKCS8),
 			},
 			},
 		},
 		},
@@ -404,7 +412,7 @@ func TestExecute(t *testing.T) {
 			data: map[string][]byte{
 			data: map[string][]byte{
 				"secret": []byte(jwkPrivRSAPKCS8 + pkcs12Cert),
 				"secret": []byte(jwkPrivRSAPKCS8 + pkcs12Cert),
 			},
 			},
-			expetedData: map[string][]byte{
+			expectedData: map[string][]byte{
 				"fn": []byte(pkcs12Cert),
 				"fn": []byte(pkcs12Cert),
 			},
 			},
 		},
 		},
@@ -420,7 +428,7 @@ func TestExecute(t *testing.T) {
 				"secret": []byte("MTIzNA=="),
 				"secret": []byte("MTIzNA=="),
 				"env":    []byte("ZGV2"),
 				"env":    []byte("ZGV2"),
 			},
 			},
-			expetedData: map[string][]byte{
+			expectedData: map[string][]byte{
 				"foo": []byte("1234"),
 				"foo": []byte("1234"),
 			},
 			},
 			expectedLabels: map[string]string{
 			expectedLabels: map[string]string{
@@ -439,13 +447,26 @@ func TestExecute(t *testing.T) {
 				"secret": []byte("MTIzNA=="),
 				"secret": []byte("MTIzNA=="),
 				"env":    []byte("ZGV2"),
 				"env":    []byte("ZGV2"),
 			},
 			},
-			expetedData: map[string][]byte{
+			expectedData: map[string][]byte{
 				"foo": []byte("1234"),
 				"foo": []byte("1234"),
 			},
 			},
 			expectedAnnotations: map[string]string{
 			expectedAnnotations: map[string]string{
 				"bar": "dev",
 				"bar": "dev",
 			},
 			},
 		},
 		},
+		{
+			name: "stringData",
+			stringDataTpl: map[string][]byte{
+				"foo": []byte("{{ .secret | b64dec }}"),
+			},
+			data: map[string][]byte{
+				"secret": []byte("MTIzNA=="),
+				"env":    []byte("ZGV2"),
+			},
+			expectedStringData: map[string]string{
+				"foo": "1234",
+			},
+		},
 	}
 	}
 
 
 	for i := range tbl {
 	for i := range tbl {
@@ -453,28 +474,123 @@ func TestExecute(t *testing.T) {
 		t.Run(row.name, func(t *testing.T) {
 		t.Run(row.name, func(t *testing.T) {
 			sec := &corev1.Secret{
 			sec := &corev1.Secret{
 				Data:       make(map[string][]byte),
 				Data:       make(map[string][]byte),
+				StringData: make(map[string]string),
 				ObjectMeta: v1.ObjectMeta{Labels: make(map[string]string), Annotations: make(map[string]string)},
 				ObjectMeta: v1.ObjectMeta{Labels: make(map[string]string), Annotations: make(map[string]string)},
 			}
 			}
-			err := Execute(row.tpl, row.labelsTpl, row.annotationsTpl, row.data, sec)
+			err := Execute(row.tpl, row.data, esapi.TemplateScopeValues, esapi.TemplateTargetData, sec)
 			if !ErrorContains(err, row.expErr) {
 			if !ErrorContains(err, row.expErr) {
 				t.Errorf("unexpected error: %s, expected: %s", err, row.expErr)
 				t.Errorf("unexpected error: %s, expected: %s", err, row.expErr)
 			}
 			}
-			if row.expetedData == nil {
-				return
+			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(row.annotationsTpl, row.data, esapi.TemplateScopeValues, esapi.TemplateTargetAnnotations, sec)
+			if !ErrorContains(err, row.expAnnoErr) {
+				t.Errorf("unexpected error: %s, expected: %s", err, row.expErr)
+			}
+			if row.expectedData != nil {
+				assert.EqualValues(t, row.expectedData, sec.Data)
 			}
 			}
-			assert.EqualValues(t, row.expetedData, sec.Data)
-			if row.expectedLabels == nil {
-				return
+			if row.expectedLabels != nil {
+				assert.EqualValues(t, row.expectedLabels, sec.ObjectMeta.Labels)
 			}
 			}
-			assert.EqualValues(t, row.expectedLabels, sec.ObjectMeta.Labels)
-			if row.expectedAnnotations == nil {
-				return
+			if row.expectedAnnotations != nil {
+				assert.EqualValues(t, row.expectedAnnotations, sec.ObjectMeta.Annotations)
 			}
 			}
-			assert.EqualValues(t, row.expectedAnnotations, sec.ObjectMeta.Annotations)
 		})
 		})
 	}
 	}
 }
 }
 
 
+func TestExecuteInvalidTemplateScope(t *testing.T) {
+	sec := &corev1.Secret{}
+	err := Execute(map[string][]byte{"foo": []byte("bar")}, nil, "invalid", esapi.TemplateTargetData, sec)
+	require.Error(t, err)
+	assert.ErrorContains(t, err, "expected 'Values' or 'KeysAndValues'")
+}
+
+func TestScopeKeysAndValues(t *testing.T) {
+	tbl := []struct {
+		name               string
+		tpl                map[string][]byte
+		target             esapi.TemplateTarget
+		data               map[string][]byte
+		expectedData       map[string][]byte
+		expectedStringData map[string]string
+		expErr             string
+	}{
+		{
+			name:   "test empty",
+			tpl:    map[string][]byte{"literal": []byte("")},
+			target: "Data",
+			data:   nil,
+		},
+		{
+			name:   "test base64",
+			tpl:    map[string][]byte{"literal": []byte("{{ .key }}: {{ .value }}")},
+			target: esapi.TemplateTargetData,
+			data: map[string][]byte{
+				"key":   []byte("foo"),
+				"value": []byte("bar"),
+			},
+			expectedData: map[string][]byte{
+				"foo": []byte("bar"),
+			},
+		},
+		{
+			name:   "test Annotations",
+			tpl:    map[string][]byte{"literal": []byte("{{ .key }}: {{ .value }}")},
+			target: esapi.TemplateTargetAnnotations,
+			data: map[string][]byte{
+				"key":   []byte("foo"),
+				"value": []byte("bar"),
+			},
+			expectedStringData: map[string]string{
+				"foo": "bar",
+			},
+		},
+		{
+			name:   "test Labels",
+			tpl:    map[string][]byte{"literal": []byte("{{ .key }}: {{ .value }}")},
+			target: esapi.TemplateTargetLabels,
+			data: map[string][]byte{
+				"key":   []byte("foo"),
+				"value": []byte("bar"),
+			},
+			expectedStringData: map[string]string{
+				"foo": "bar",
+			},
+		},
+	}
+	for i := range tbl {
+		row := tbl[i]
+		t.Run(row.name, func(t *testing.T) {
+			sec := &corev1.Secret{
+				Data:       make(map[string][]byte),
+				StringData: make(map[string]string),
+				ObjectMeta: v1.ObjectMeta{Labels: make(map[string]string), Annotations: make(map[string]string)},
+			}
+			err := Execute(row.tpl, row.data, esapi.TemplateScopeKeysAndValues, row.target, sec)
+			if !ErrorContains(err, row.expErr) {
+				t.Errorf("unexpected error: %s, expected: %s", err, row.expErr)
+			}
+			switch row.target {
+			case esapi.TemplateTargetData:
+				if row.expectedData != nil {
+					assert.EqualValues(t, row.expectedData, sec.Data)
+				}
+			case esapi.TemplateTargetLabels:
+				if row.expectedStringData != nil {
+					assert.EqualValues(t, row.expectedStringData, sec.Labels)
+				}
+			case esapi.TemplateTargetAnnotations:
+				if row.expectedStringData != nil {
+					assert.EqualValues(t, row.expectedStringData, sec.Annotations)
+				}
+			}
+		})
+	}
+}
 func ErrorContains(out error, want string) bool {
 func ErrorContains(out error, want string) bool {
 	if out == nil {
 	if out == nil {
 		return want == ""
 		return want == ""