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
 	Data map[string]string `json:"data,omitempty"`
-
 	// +optional
 	TemplateFrom []TemplateFrom `json:"templateFrom,omitempty"`
 }
@@ -110,13 +109,32 @@ const (
 	TemplateEngineV2 TemplateEngineVersion = "v2"
 )
 
-// +kubebuilder:validation:MinProperties=1
-// +kubebuilder:validation:MaxProperties=1
 type TemplateFrom struct {
 	ConfigMap *TemplateRef `json:"configMap,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 {
 	Name  string            `json:"name"`
 	Items []TemplateRefItem `json:"items"`
@@ -124,6 +142,8 @@ type TemplateRef struct {
 
 type TemplateRefItem struct {
 	Key string `json:"key"`
+	// +kubebuilder:default="Values"
+	TemplateAs TemplateScope `json:"templateAs,omitempty"`
 }
 
 // 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)
 		(*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.

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

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

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

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

+ 22 - 4
deploy/crds/bundle.yaml

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

+ 80 - 0
docs/api/spec.md

@@ -4833,6 +4833,30 @@ TemplateRef
 <td>
 </td>
 </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>
 </table>
 <h3 id="external-secrets.io/v1beta1.TemplateRef">TemplateRef
@@ -4901,8 +4925,64 @@ string
 <td>
 </td>
 </tr>
+<tr>
+<td>
+<code>templateAs</code></br>
+<em>
+<a href="#external-secrets.io/v1beta1.TemplateScope">
+TemplateScope
+</a>
+</em>
+</td>
+<td>
+</td>
+</tr>
 </tbody>
 </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>
 <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' %}
 ```
 
+`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
 
 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
         type: graphite
         access: proxy
-        url: http://localhost:8080
+        url: "{{ .uri }}"
         password: "{{ .password }}"
         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
 kind: ExternalSecret
@@ -25,12 +39,24 @@ spec:
     template:
       engineVersion: v2
       templateFrom:
-      - configMap:
+      - target: Data
+        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: 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:
   - secretKey: user
     remoteRef:
@@ -38,4 +64,7 @@ spec:
   - secretKey: password
     remoteRef:
       key: /grafana/password
+  - secretKey: uri
+    remoteRef:
+      key: /grafana/uri
 {% 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"
 
 	//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"
 	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
 	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"
@@ -34,6 +34,7 @@ type TestCase struct {
 	Framework              *Framework
 	ExternalSecret         *esv1beta1.ExternalSecret
 	ExternalSecretV1Alpha1 *esv1alpha1.ExternalSecret
+	AdditionalObjects      []client.Object
 	Secrets                map[string]SecretEntry
 	ExpectedSecret         *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)
 			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
 		if tc.ExternalSecret.Spec.Target.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"
 	"k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	"github.com/external-secrets/external-secrets-e2e/framework"
 	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.
 func JSONDataFromSync(f *framework.Framework) (string, func(*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.SyncV1Alpha1, 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
 		framework.Compose(withCertAuth, f, common.FindByName, 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"
 )
 
+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:
 // * template.Data (highest precedence)
 // * template.templateFrom
@@ -42,47 +155,38 @@ func (r *Reconciler) applyTemplate(ctx context.Context, es *esv1beta1.ExternalSe
 		secret.Annotations[esv1beta1.AnnotationDataHash] = utils.ObjectHash(secret.Data)
 		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 {
 		return fmt.Errorf(errFetchTplFrom, err)
 	}
-
 	// 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
-	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 {
-		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 {
 		return fmt.Errorf(errExecTpl, err)
 	}
-
 	// if no data was provided by template fallback
 	// to value from the provider
 	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.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))
 		}
 	}
-
-	// 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) {
 		const secretVal = "someValue"
 		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"))
 		}
 	}
+	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) {
 		const secretVal = "someValue"
@@ -1794,6 +1915,8 @@ var _ = Describe("ExternalSecret controller", func() {
 		Entry("should sync with template", syncWithTemplate),
 		Entry("should sync with template engine v2", syncWithTemplateV2),
 		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 be able to use only metadata from template", onlyMetadataFromTemplate),
 		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"
 )
 
-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) {
 	switch version {

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

@@ -27,6 +27,8 @@ import (
 	"github.com/youmark/pkcs8"
 	"golang.org/x/crypto/pkcs12"
 	corev1 "k8s.io/api/core/v1"
+
+	esapi "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 )
 
 var tplFuncs = tpl.FuncMap{
@@ -70,7 +72,7 @@ const (
 )
 
 // 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 {
 		return nil
 	}

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

@@ -349,7 +349,7 @@ func TestExecute(t *testing.T) {
 				Data:       make(map[string][]byte),
 				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) {
 				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"
 	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{
@@ -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)
 		if err != nil {
 			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 {
-			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
 }

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

@@ -20,8 +20,11 @@ import (
 
 	"github.com/google/go-cmp/cmp"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 	corev1 "k8s.io/api/core/v1"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	esapi "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 )
 
 const (
@@ -140,11 +143,16 @@ func TestExecute(t *testing.T) {
 		tpl                 map[string][]byte
 		labelsTpl           map[string][]byte
 		annotationsTpl      map[string][]byte
+		stringDataTpl       map[string][]byte
 		data                map[string][]byte
-		expetedData         map[string][]byte
+		expectedData        map[string][]byte
+		expectedStringData  map[string]string
 		expectedLabels      map[string]string
 		expectedAnnotations map[string]string
 		expErr              string
+		expLblErr           string
+		expAnnoErr          string
+		expStrErr           string
 	}{
 		{
 			name:           "test empty",
@@ -161,7 +169,7 @@ func TestExecute(t *testing.T) {
 			data: map[string][]byte{
 				"secret": []byte("MTIzNA=="),
 			},
-			expetedData: map[string][]byte{
+			expectedData: map[string][]byte{
 				"foo": []byte("1234"),
 			},
 		},
@@ -173,7 +181,7 @@ func TestExecute(t *testing.T) {
 			data: map[string][]byte{
 				"secret": []byte(`{"foo": "bar"}`),
 			},
-			expetedData: map[string][]byte{
+			expectedData: map[string][]byte{
 				"foo": []byte("bar"),
 			},
 		},
@@ -185,7 +193,7 @@ func TestExecute(t *testing.T) {
 			data: map[string][]byte{
 				"secret": []byte(`{"foo": {"baz":"bang"}}`),
 			},
-			expetedData: map[string][]byte{
+			expectedData: map[string][]byte{
 				"foo": []byte(`{"baz":"bang"}`),
 			},
 		},
@@ -197,7 +205,7 @@ func TestExecute(t *testing.T) {
 			data: map[string][]byte{
 				"secret": []byte(`{"foo": "bar"}`),
 			},
-			expetedData: map[string][]byte{
+			expectedData: map[string][]byte{
 				"foo": []byte(`foo: bar`),
 			},
 		},
@@ -209,7 +217,7 @@ func TestExecute(t *testing.T) {
 			data: map[string][]byte{
 				"secret": []byte(`foo: bar`),
 			},
-			expetedData: map[string][]byte{
+			expectedData: map[string][]byte{
 				"foo": []byte(`{"foo":"bar"}`),
 			},
 		},
@@ -221,7 +229,7 @@ func TestExecute(t *testing.T) {
 			data: map[string][]byte{
 				"path": []byte(`foo/bar/baz.exe`),
 			},
-			expetedData: map[string][]byte{
+			expectedData: map[string][]byte{
 				"foo": []byte(`.exe`),
 			},
 		},
@@ -233,7 +241,7 @@ func TestExecute(t *testing.T) {
 			data: map[string][]byte{
 				"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`),
 			},
 		},
@@ -253,7 +261,7 @@ func TestExecute(t *testing.T) {
 				"user":     []byte(`foobert`),
 				"password": []byte("harharhar"),
 			},
-			expetedData: map[string][]byte{
+			expectedData: map[string][]byte{
 				"cfg": []byte(`
 		datasources:
 		- name: Graphite
@@ -270,7 +278,7 @@ func TestExecute(t *testing.T) {
 				"foo": []byte(`{{ "123412341234" | b64enc | b64dec }}`),
 			},
 			data: map[string][]byte{},
-			expetedData: map[string][]byte{
+			expectedData: map[string][]byte{
 				"foo": []byte("123412341234"),
 			},
 		},
@@ -283,7 +291,7 @@ func TestExecute(t *testing.T) {
 			data: map[string][]byte{
 				"secret": []byte(pkcs12ContentNoPass),
 			},
-			expetedData: map[string][]byte{
+			expectedData: map[string][]byte{
 				"key":  []byte(pkcs12Key),
 				"cert": []byte(pkcs12Cert),
 			},
@@ -297,7 +305,7 @@ func TestExecute(t *testing.T) {
 			data: map[string][]byte{
 				"secret": []byte(pkcs12ContentWithPass),
 			},
-			expetedData: map[string][]byte{
+			expectedData: map[string][]byte{
 				"key":  []byte(pkcs12Key),
 				"cert": []byte(pkcs12Cert),
 			},
@@ -356,7 +364,7 @@ func TestExecute(t *testing.T) {
 			data: map[string][]byte{
 				"secret": []byte(jwkPubRSA),
 			},
-			expetedData: map[string][]byte{
+			expectedData: map[string][]byte{
 				"fn": []byte(jwkPubRSAPKIX),
 			},
 		},
@@ -368,7 +376,7 @@ func TestExecute(t *testing.T) {
 			data: map[string][]byte{
 				"secret": []byte(jwkPrivRSA),
 			},
-			expetedData: map[string][]byte{
+			expectedData: map[string][]byte{
 				"fn": []byte(jwkPrivRSAPKCS8),
 			},
 		},
@@ -380,7 +388,7 @@ func TestExecute(t *testing.T) {
 			data: map[string][]byte{
 				"secret": []byte(jwkPubEC),
 			},
-			expetedData: map[string][]byte{
+			expectedData: map[string][]byte{
 				"fn": []byte(jwkPubECPKIX),
 			},
 		},
@@ -392,7 +400,7 @@ func TestExecute(t *testing.T) {
 			data: map[string][]byte{
 				"secret": []byte(jwkPrivEC),
 			},
-			expetedData: map[string][]byte{
+			expectedData: map[string][]byte{
 				"fn": []byte(jwkPrivECPKCS8),
 			},
 		},
@@ -404,7 +412,7 @@ func TestExecute(t *testing.T) {
 			data: map[string][]byte{
 				"secret": []byte(jwkPrivRSAPKCS8 + pkcs12Cert),
 			},
-			expetedData: map[string][]byte{
+			expectedData: map[string][]byte{
 				"fn": []byte(pkcs12Cert),
 			},
 		},
@@ -420,7 +428,7 @@ func TestExecute(t *testing.T) {
 				"secret": []byte("MTIzNA=="),
 				"env":    []byte("ZGV2"),
 			},
-			expetedData: map[string][]byte{
+			expectedData: map[string][]byte{
 				"foo": []byte("1234"),
 			},
 			expectedLabels: map[string]string{
@@ -439,13 +447,26 @@ func TestExecute(t *testing.T) {
 				"secret": []byte("MTIzNA=="),
 				"env":    []byte("ZGV2"),
 			},
-			expetedData: map[string][]byte{
+			expectedData: map[string][]byte{
 				"foo": []byte("1234"),
 			},
 			expectedAnnotations: map[string]string{
 				"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 {
@@ -453,28 +474,123 @@ func TestExecute(t *testing.T) {
 		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.labelsTpl, row.annotationsTpl, row.data, sec)
+			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)
 			}
-			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 {
 	if out == nil {
 		return want == ""