Browse Source

feat: add templating to PushSecret (#2926)

* feat: add templating to PushSecret

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

* adding unit tests around templating basic concepts and verifying output

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

* extracting some of the common functions of the parser

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

* remove some more duplication

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

* removed commented out code segment

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

* added documentation for templating feature

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

* simplified the templating for annotations and labels

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

---------

Signed-off-by: Gergely Brautigam <182850+Skarlso@users.noreply.github.com>
Gergely Brautigam 2 years ago
parent
commit
d6e24a82bd

+ 5 - 0
apis/externalsecrets/v1alpha1/pushsecret_types.go

@@ -18,6 +18,8 @@ import (
 	corev1 "k8s.io/api/core/v1"
 	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 )
 
 const (
@@ -60,6 +62,9 @@ type PushSecretSpec struct {
 	Selector PushSecretSelector `json:"selector"`
 	// Secret Data that should be pushed to providers
 	Data []PushSecretData `json:"data,omitempty"`
+	// Template defines a blueprint for the created Secret resource.
+	// +optional
+	Template *esv1beta1.ExternalSecretTemplate `json:"template,omitempty"`
 }
 
 type PushSecretSecret struct {

+ 6 - 0
apis/externalsecrets/v1alpha1/zz_generated.deepcopy.go

@@ -19,6 +19,7 @@ limitations under the License.
 package v1alpha1
 
 import (
+	"github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 	metav1 "github.com/external-secrets/external-secrets/apis/meta/v1"
 	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	"k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -1195,6 +1196,11 @@ func (in *PushSecretSpec) DeepCopyInto(out *PushSecretSpec) {
 			(*in)[i].DeepCopyInto(&(*out)[i])
 		}
 	}
+	if in.Template != nil {
+		in, out := &in.Template, &out.Template
+		*out = new(v1beta1.ExternalSecretTemplate)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSecretSpec.

+ 98 - 0
config/crds/bases/external-secrets.io_pushsecrets.yaml

@@ -162,6 +162,104 @@ spec:
                 required:
                 - secret
                 type: object
+              template:
+                description: Template defines a blueprint for the created Secret resource.
+                properties:
+                  data:
+                    additionalProperties:
+                      type: string
+                    type: object
+                  engineVersion:
+                    default: v2
+                    description: EngineVersion specifies the template engine version
+                      that should be used to compile/execute the template specified
+                      in .data and .templateFrom[].
+                    enum:
+                    - v1
+                    - v2
+                    type: string
+                  mergePolicy:
+                    default: Replace
+                    enum:
+                    - Replace
+                    - Merge
+                    type: string
+                  metadata:
+                    description: ExternalSecretTemplateMetadata defines metadata fields
+                      for the Secret blueprint.
+                    properties:
+                      annotations:
+                        additionalProperties:
+                          type: string
+                        type: object
+                      labels:
+                        additionalProperties:
+                          type: string
+                        type: object
+                    type: object
+                  templateFrom:
+                    items:
+                      properties:
+                        configMap:
+                          properties:
+                            items:
+                              items:
+                                properties:
+                                  key:
+                                    type: string
+                                  templateAs:
+                                    default: Values
+                                    enum:
+                                    - Values
+                                    - KeysAndValues
+                                    type: string
+                                required:
+                                - key
+                                type: object
+                              type: array
+                            name:
+                              type: string
+                          required:
+                          - items
+                          - name
+                          type: object
+                        literal:
+                          type: string
+                        secret:
+                          properties:
+                            items:
+                              items:
+                                properties:
+                                  key:
+                                    type: string
+                                  templateAs:
+                                    default: Values
+                                    enum:
+                                    - Values
+                                    - KeysAndValues
+                                    type: string
+                                required:
+                                - key
+                                type: object
+                              type: array
+                            name:
+                              type: string
+                          required:
+                          - items
+                          - name
+                          type: object
+                        target:
+                          default: Data
+                          enum:
+                          - Data
+                          - Annotations
+                          - Labels
+                          type: string
+                      type: object
+                    type: array
+                  type:
+                    type: string
+                type: object
             required:
             - secretStoreRefs
             - selector

+ 95 - 0
deploy/crds/bundle.yaml

@@ -4384,6 +4384,101 @@ spec:
                   required:
                     - secret
                   type: object
+                template:
+                  description: Template defines a blueprint for the created Secret resource.
+                  properties:
+                    data:
+                      additionalProperties:
+                        type: string
+                      type: object
+                    engineVersion:
+                      default: v2
+                      description: EngineVersion specifies the template engine version that should be used to compile/execute the template specified in .data and .templateFrom[].
+                      enum:
+                        - v1
+                        - v2
+                      type: string
+                    mergePolicy:
+                      default: Replace
+                      enum:
+                        - Replace
+                        - Merge
+                      type: string
+                    metadata:
+                      description: ExternalSecretTemplateMetadata defines metadata fields for the Secret blueprint.
+                      properties:
+                        annotations:
+                          additionalProperties:
+                            type: string
+                          type: object
+                        labels:
+                          additionalProperties:
+                            type: string
+                          type: object
+                      type: object
+                    templateFrom:
+                      items:
+                        properties:
+                          configMap:
+                            properties:
+                              items:
+                                items:
+                                  properties:
+                                    key:
+                                      type: string
+                                    templateAs:
+                                      default: Values
+                                      enum:
+                                        - Values
+                                        - KeysAndValues
+                                      type: string
+                                  required:
+                                    - key
+                                  type: object
+                                type: array
+                              name:
+                                type: string
+                            required:
+                              - items
+                              - name
+                            type: object
+                          literal:
+                            type: string
+                          secret:
+                            properties:
+                              items:
+                                items:
+                                  properties:
+                                    key:
+                                      type: string
+                                    templateAs:
+                                      default: Values
+                                      enum:
+                                        - Values
+                                        - KeysAndValues
+                                      type: string
+                                  required:
+                                    - key
+                                  type: object
+                                type: array
+                              name:
+                                type: string
+                            required:
+                              - items
+                              - name
+                            type: object
+                          target:
+                            default: Data
+                            enum:
+                              - Data
+                              - Annotations
+                              - Labels
+                            type: string
+                        type: object
+                      type: array
+                    type:
+                      type: string
+                  type: object
               required:
                 - secretStoreRefs
                 - selector

+ 9 - 1
docs/api/pushsecret.md

@@ -3,8 +3,16 @@
 The `PushSecret` is namespaced and it describes what data should be pushed to the secret provider.
 
 * tells the operator what secrets should be pushed by using `spec.selector`.
-* you can specify what secret keys should be pushed by using `spec.data`
+* you can specify what secret keys should be pushed by using `spec.data`.
+* you can also template the resulting property values using [templating](#templating).
 
 ``` yaml
 {% include 'full-pushsecret.yaml' %}
 ```
+
+## Templating
+
+When the controller reconciles the `PushSecret` it will use the `spec.template` as a blueprint to construct a new property.
+You can use golang templates to define the blueprint and use template functions to transform the defined properties.
+You can also pull in `ConfigMaps` that contain golang-template data using `templateFrom`.
+See [advanced templating](../guides/templating.md) for details.

+ 10 - 0
docs/guides/templating.md

@@ -112,6 +112,16 @@ You can achieve that by using the `filterPEM` function to extract a specific typ
 {% include 'filterpem-template-v2-external-secret.yaml' %}
 ```
 
+## Templating with PushSecret
+
+`PushSecret` templating is much like `ExternalSecrets` templating. In-fact under the hood, it's using the same data structure.
+Which means, anything described in the above should be possible with push secret as well resulting in a templated secret
+created at the provider.
+
+```yaml
+{% include 'template-v2-push-secret.yaml' %}
+```
+
 ## Helper functions
 
 !!! info inline end

+ 16 - 0
docs/snippets/full-pushsecret.yaml

@@ -1,3 +1,4 @@
+{% raw %}
 apiVersion: external-secrets.io/v1alpha1
 kind: PushSecret
 metadata:
@@ -12,8 +13,23 @@ spec:
   selector:
     secret:
       name: pokedex-credentials # Source Kubernetes secret to be pushed
+  template:
+    metadata:
+      annotations: { }
+      labels: { }
+    data:
+      best-pokemon: "{{ .best-pokemon | toString | upper }} is the really best!"
+    # Uses an existing template from configmap
+    # Secret is fetched, merged and templated within the referenced configMap data
+    # It does not update the configmap, it creates a secret with: data["alertmanager.yml"] = ...result...
+    templateFrom:
+      - configMap:
+          name: application-config-tmpl
+          items:
+            - key: config.yml
   data:
     - match:
         secretKey: best-pokemon # Source Kubernetes secret key to be pushed
         remoteRef:
           remoteKey: my-first-parameter # Remote reference (where the secret is going to be pushed)
+{% endraw %}

+ 18 - 0
docs/snippets/template-v2-push-secret.yaml

@@ -0,0 +1,18 @@
+{% raw %}
+apiVersion: external-secrets.io/v1beta1
+kind: PushSecret
+metadata:
+  name: template
+spec:
+  # ...
+  template:
+    engineVersion: v2
+    data:
+      token: "{{ .token | toString | upper }} was templated"
+  data:
+    - match:
+        secretKey: token
+        remoteRef:
+          remoteKey: create-secret-name
+          property: token
+{% endraw %}

+ 10 - 177
pkg/controllers/externalsecret/externalsecret_controller_template.go

@@ -19,129 +19,14 @@ import (
 	"fmt"
 
 	v1 "k8s.io/api/core/v1"
-	"k8s.io/apimachinery/pkg/types"
-	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
-	// Loading registered providers.
-	_ "github.com/external-secrets/external-secrets/pkg/provider/register"
+	"github.com/external-secrets/external-secrets/pkg/controllers/templating"
+	_ "github.com/external-secrets/external-secrets/pkg/provider/register" // Loading registered providers.
 	"github.com/external-secrets/external-secrets/pkg/template"
 	"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(_ 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
@@ -167,14 +52,14 @@ func (r *Reconciler) applyTemplate(ctx context.Context, es *esv1beta1.ExternalSe
 		return err
 	}
 
-	p := Parser{
-		client:       r.Client,
-		targetSecret: secret,
-		dataMap:      dataMap,
-		exec:         execute,
+	p := templating.Parser{
+		Client:       r.Client,
+		TargetSecret: secret,
+		DataMap:      dataMap,
+		Exec:         execute,
 	}
 	// apply templates defined in template.templateFrom
-	err = p.MergeTemplateFrom(ctx, es)
+	err = p.MergeTemplateFrom(ctx, es.Namespace, es.Spec.Target.Template)
 	if err != nil {
 		return fmt.Errorf(errFetchTplFrom, err)
 	}
@@ -212,7 +97,7 @@ func setMetadata(secret *v1.Secret, es *esv1beta1.ExternalSecret) error {
 	}
 	// Clean up Labels and Annotations added by the operator
 	// so that it won't leave outdated ones
-	labelKeys, err := getManagedLabelKeys(secret, es.Name)
+	labelKeys, err := templating.GetManagedLabelKeys(secret, es.Name)
 	if err != nil {
 		return err
 	}
@@ -220,7 +105,7 @@ func setMetadata(secret *v1.Secret, es *esv1beta1.ExternalSecret) error {
 		delete(secret.ObjectMeta.Labels, key)
 	}
 
-	annotationKeys, err := getManagedAnnotationKeys(secret, es.Name)
+	annotationKeys, err := templating.GetManagedAnnotationKeys(secret, es.Name)
 	if err != nil {
 		return err
 	}
@@ -239,55 +124,3 @@ func setMetadata(secret *v1.Secret, es *esv1beta1.ExternalSecret) error {
 	utils.MergeStringMap(secret.ObjectMeta.Annotations, es.Spec.Target.Template.Metadata.Annotations)
 	return nil
 }
-
-func getManagedAnnotationKeys(secret *v1.Secret, fieldOwner string) ([]string, error) {
-	return getManagedFieldKeys(secret, fieldOwner, func(fields map[string]interface{}) []string {
-		metadataFields, exists := fields["f:metadata"]
-		if !exists {
-			return nil
-		}
-		mf, ok := metadataFields.(map[string]interface{})
-		if !ok {
-			return nil
-		}
-		annotationFields, exists := mf["f:annotations"]
-		if !exists {
-			return nil
-		}
-		af, ok := annotationFields.(map[string]interface{})
-		if !ok {
-			return nil
-		}
-		var keys []string
-		for k := range af {
-			keys = append(keys, k)
-		}
-		return keys
-	})
-}
-
-func getManagedLabelKeys(secret *v1.Secret, fieldOwner string) ([]string, error) {
-	return getManagedFieldKeys(secret, fieldOwner, func(fields map[string]interface{}) []string {
-		metadataFields, exists := fields["f:metadata"]
-		if !exists {
-			return nil
-		}
-		mf, ok := metadataFields.(map[string]interface{})
-		if !ok {
-			return nil
-		}
-		labelFields, exists := mf["f:labels"]
-		if !exists {
-			return nil
-		}
-		lf, ok := labelFields.(map[string]interface{})
-		if !ok {
-			return nil
-		}
-		var keys []string
-		for k := range lf {
-			keys = append(keys, k)
-		}
-		return keys
-	})
-}

+ 12 - 10
pkg/controllers/pushsecret/pushsecret_controller.go

@@ -41,16 +41,13 @@ import (
 )
 
 const (
-	errFailedGetSecret        = "could not get source secret"
-	errPatchStatus            = "error merging"
-	errGetSecretStore         = "could not get SecretStore %q, %w"
-	errGetClusterSecretStore  = "could not get ClusterSecretStore %q, %w"
-	errGetProviderFailed      = "could not start provider"
-	errGetSecretsClientFailed = "could not start secrets client"
-	errCloseStoreClient       = "error when calling provider close method"
-	errSetSecretFailed        = "could not write remote ref %v to target secretstore %v: %v"
-	errFailedSetSecret        = "set secret failed: %v"
-	pushSecretFinalizer       = "pushsecret.externalsecrets.io/finalizer"
+	errFailedGetSecret       = "could not get source secret"
+	errPatchStatus           = "error merging"
+	errGetSecretStore        = "could not get SecretStore %q, %w"
+	errGetClusterSecretStore = "could not get ClusterSecretStore %q, %w"
+	errSetSecretFailed       = "could not write remote ref %v to target secretstore %v: %v"
+	errFailedSetSecret       = "set secret failed: %v"
+	pushSecretFinalizer      = "pushsecret.externalsecrets.io/finalizer"
 )
 
 type Reconciler struct {
@@ -153,6 +150,11 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
 
 		return ctrl.Result{}, err
 	}
+
+	if err := r.applyTemplate(ctx, &ps, secret); err != nil {
+		return ctrl.Result{}, err
+	}
+
 	syncedSecrets, err := r.PushSecretToProviders(ctx, secretStores, ps, secret, mgr)
 	if err != nil {
 		if errors.Is(err, locks.ErrConflict) {

+ 104 - 0
pkg/controllers/pushsecret/pushsecret_controller_template.go

@@ -0,0 +1,104 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package pushsecret
+
+import (
+	"context"
+	"fmt"
+
+	v1 "k8s.io/api/core/v1"
+
+	"github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	"github.com/external-secrets/external-secrets/pkg/controllers/templating"
+	_ "github.com/external-secrets/external-secrets/pkg/provider/register" // Loading registered providers.
+	"github.com/external-secrets/external-secrets/pkg/template"
+	"github.com/external-secrets/external-secrets/pkg/utils"
+)
+
+const (
+	errFetchTplFrom = "error fetching templateFrom data: %w"
+	errExecTpl      = "could not execute template: %w"
+)
+
+// applyTemplate merges template in the following order:
+// * template.Data (highest precedence)
+// * template.templateFrom
+// * secret via ps.data or ps.dataFrom.
+// Apply template modifications for the source secret. These modifications will only live in memory as we will
+// never modify it.
+func (r *Reconciler) applyTemplate(ctx context.Context, ps *v1alpha1.PushSecret, secret *v1.Secret) error {
+	// no template: nothing to do
+	if ps.Spec.Template == nil {
+		return nil
+	}
+
+	if err := setMetadata(secret, ps); err != nil {
+		return err
+	}
+
+	execute, err := template.EngineForVersion(esv1beta1.TemplateEngineV2)
+	if err != nil {
+		return err
+	}
+
+	p := templating.Parser{
+		Client:       r.Client,
+		TargetSecret: secret,
+		DataMap:      secret.Data,
+		Exec:         execute,
+	}
+
+	// apply templates defined in template.templateFrom
+	err = p.MergeTemplateFrom(ctx, ps.Namespace, ps.Spec.Template)
+	if err != nil {
+		return fmt.Errorf(errFetchTplFrom, err)
+	}
+	// explicitly defined template.Data takes precedence over templateFrom
+	err = p.MergeMap(ps.Spec.Template.Data, esv1beta1.TemplateTargetData)
+	if err != nil {
+		return fmt.Errorf(errExecTpl, err)
+	}
+
+	// get template data for labels
+	err = p.MergeMap(ps.Spec.Template.Metadata.Labels, esv1beta1.TemplateTargetLabels)
+	if err != nil {
+		return fmt.Errorf(errExecTpl, err)
+	}
+	// get template data for annotations
+	err = p.MergeMap(ps.Spec.Template.Metadata.Annotations, esv1beta1.TemplateTargetAnnotations)
+	if err != nil {
+		return fmt.Errorf(errExecTpl, err)
+	}
+
+	return nil
+}
+
+// setMetadata sets Labels and Annotations in the source secret, but we will never write them back.
+// It is only set to satisfy templated changes.
+func setMetadata(secret *v1.Secret, ps *v1alpha1.PushSecret) error {
+	if secret.Labels == nil {
+		secret.Labels = make(map[string]string)
+	}
+	if secret.Annotations == nil {
+		secret.Annotations = make(map[string]string)
+	}
+
+	secret.Type = ps.Spec.Template.Type
+	utils.MergeStringMap(secret.ObjectMeta.Labels, ps.Spec.Template.Metadata.Labels)
+	utils.MergeStringMap(secret.ObjectMeta.Annotations, ps.Spec.Template.Metadata.Annotations)
+
+	return nil
+}

+ 64 - 0
pkg/controllers/pushsecret/pushsecret_controller_test.go

@@ -206,6 +206,69 @@ var _ = Describe("ExternalSecret controller", func() {
 			return true
 		}
 	}
+
+	// if target Secret name is not specified it should use the ExternalSecret name.
+	syncSuccessfullyWithTemplate := func(tc *testCase) {
+		fakeProvider.SetSecretFn = func() error {
+			return nil
+		}
+		tc.pushsecret = &v1alpha1.PushSecret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      PushSecretName,
+				Namespace: PushSecretNamespace,
+			},
+			Spec: v1alpha1.PushSecretSpec{
+				SecretStoreRefs: []v1alpha1.PushSecretStoreRef{
+					{
+						Name: PushSecretStore,
+						Kind: "SecretStore",
+					},
+				},
+				Selector: v1alpha1.PushSecretSelector{
+					Secret: v1alpha1.PushSecretSecret{
+						Name: SecretName,
+					},
+				},
+				Data: []v1alpha1.PushSecretData{
+					{
+						Match: v1alpha1.PushSecretMatch{
+							SecretKey: "key",
+							RemoteRef: v1alpha1.PushSecretRemoteRef{
+								RemoteKey: "path/to/key",
+							},
+						},
+					},
+				},
+				Template: &v1beta1.ExternalSecretTemplate{
+					Metadata: v1beta1.ExternalSecretTemplateMetadata{
+						Labels: map[string]string{
+							"foos": "ball",
+						},
+						Annotations: map[string]string{
+							"hihi": "ga",
+						},
+					},
+					Type:          v1.SecretTypeOpaque,
+					EngineVersion: v1beta1.TemplateEngineV2,
+					Data: map[string]string{
+						"key": "{{ .key | toString | upper }} was templated",
+					},
+				},
+			},
+		}
+		tc.assert = func(ps *v1alpha1.PushSecret, secret *v1.Secret) bool {
+			Eventually(func() bool {
+				By("checking if Provider value got updated")
+				providerValue, ok := fakeProvider.SetSecretArgs[ps.Spec.Data[0].Match.RemoteRef.RemoteKey]
+				if !ok {
+					return false
+				}
+				got := providerValue.Value
+				return bytes.Equal(got, []byte("VALUE was templated"))
+			}, time.Second*10, time.Second).Should(BeTrue())
+			return true
+		}
+	}
 	// if target Secret name is not specified it should use the ExternalSecret name.
 	syncAndDeleteSuccessfully := func(tc *testCase) {
 		fakeProvider.SetSecretFn = func() error {
@@ -705,6 +768,7 @@ var _ = Describe("ExternalSecret controller", func() {
 			// this must be optional so we can test faulty es configuration
 		},
 		Entry("should sync", syncSuccessfully),
+		Entry("should sync with template", syncSuccessfullyWithTemplate),
 		Entry("should delete if DeletionPolicy=Delete", syncAndDeleteSuccessfully),
 		Entry("should track deletion tasks if Delete fails", failDelete),
 		Entry("should track deleted stores if Delete fails", failDeleteStore),

+ 229 - 0
pkg/controllers/templating/parser.go

@@ -0,0 +1,229 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package templating
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"strings"
+
+	v1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	esv1beta1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
+	"github.com/external-secrets/external-secrets/pkg/template"
+)
+
+const fieldOwnerTemplate = "externalsecrets.external-secrets.io/%v"
+
+var (
+	errTplCMMissingKey  = "error in configmap %s: missing key %s"
+	errTplSecMissingKey = "error in secret %s: missing key %s"
+	errExecTpl          = "could not execute template: %w"
+)
+
+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(_ 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, namespace string, template *esv1beta1.ExternalSecretTemplate) error {
+	if template == nil {
+		return nil
+	}
+
+	for _, tpl := range template.TemplateFrom {
+		err := p.MergeConfigMap(ctx, namespace, tpl)
+		if err != nil {
+			return err
+		}
+		err = p.MergeSecret(ctx, 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
+}
+
+func GetManagedAnnotationKeys(secret *v1.Secret, fieldOwner string) ([]string, error) {
+	return getManagedFieldKeys(secret, fieldOwner, func(fields map[string]interface{}) []string {
+		metadataFields, exists := fields["f:metadata"]
+		if !exists {
+			return nil
+		}
+		mf, ok := metadataFields.(map[string]interface{})
+		if !ok {
+			return nil
+		}
+		annotationFields, exists := mf["f:annotations"]
+		if !exists {
+			return nil
+		}
+		af, ok := annotationFields.(map[string]interface{})
+		if !ok {
+			return nil
+		}
+		var keys []string
+		for k := range af {
+			keys = append(keys, k)
+		}
+		return keys
+	})
+}
+
+func GetManagedLabelKeys(secret *v1.Secret, fieldOwner string) ([]string, error) {
+	return getManagedFieldKeys(secret, fieldOwner, func(fields map[string]interface{}) []string {
+		metadataFields, exists := fields["f:metadata"]
+		if !exists {
+			return nil
+		}
+		mf, ok := metadataFields.(map[string]interface{})
+		if !ok {
+			return nil
+		}
+		labelFields, exists := mf["f:labels"]
+		if !exists {
+			return nil
+		}
+		lf, ok := labelFields.(map[string]interface{})
+		if !ok {
+			return nil
+		}
+		var keys []string
+		for k := range lf {
+			keys = append(keys, k)
+		}
+		return keys
+	})
+}
+
+func getManagedFieldKeys(
+	secret *v1.Secret,
+	fieldOwner string,
+	process func(fields map[string]interface{}) []string,
+) ([]string, error) {
+	fqdn := fmt.Sprintf(fieldOwnerTemplate, fieldOwner)
+	var keys []string
+	for _, v := range secret.ObjectMeta.ManagedFields {
+		if v.Manager != fqdn {
+			continue
+		}
+		fields := make(map[string]interface{})
+		err := json.Unmarshal(v.FieldsV1.Raw, &fields)
+		if err != nil {
+			return nil, fmt.Errorf("error unmarshaling managed fields: %w", err)
+		}
+		for _, key := range process(fields) {
+			if key == "." {
+				continue
+			}
+			keys = append(keys, strings.TrimPrefix(key, "f:"))
+		}
+	}
+	return keys, nil
+}