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

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

@@ -19,6 +19,7 @@ limitations under the License.
 package v1alpha1
 package v1alpha1
 
 
 import (
 import (
+	"github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
 	metav1 "github.com/external-secrets/external-secrets/apis/meta/v1"
 	metav1 "github.com/external-secrets/external-secrets/apis/meta/v1"
 	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	"k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -1195,6 +1196,11 @@ func (in *PushSecretSpec) DeepCopyInto(out *PushSecretSpec) {
 			(*in)[i].DeepCopyInto(&(*out)[i])
 			(*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.
 // 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:
                 required:
                 - secret
                 - secret
                 type: object
                 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:
             required:
             - secretStoreRefs
             - secretStoreRefs
             - selector
             - selector

+ 95 - 0
deploy/crds/bundle.yaml

@@ -4384,6 +4384,101 @@ spec:
                   required:
                   required:
                     - secret
                     - secret
                   type: object
                   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:
               required:
                 - secretStoreRefs
                 - secretStoreRefs
                 - selector
                 - 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.
 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`.
 * 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
 ``` yaml
 {% include 'full-pushsecret.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' %}
 {% 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
 ## Helper functions
 
 
 !!! info inline end
 !!! info inline end

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

@@ -1,3 +1,4 @@
+{% raw %}
 apiVersion: external-secrets.io/v1alpha1
 apiVersion: external-secrets.io/v1alpha1
 kind: PushSecret
 kind: PushSecret
 metadata:
 metadata:
@@ -12,8 +13,23 @@ spec:
   selector:
   selector:
     secret:
     secret:
       name: pokedex-credentials # Source Kubernetes secret to be pushed
       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:
   data:
     - match:
     - match:
         secretKey: best-pokemon # Source Kubernetes secret key to be pushed
         secretKey: best-pokemon # Source Kubernetes secret key to be pushed
         remoteRef:
         remoteRef:
           remoteKey: my-first-parameter # Remote reference (where the secret is going to be pushed)
           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"
 	"fmt"
 
 
 	v1 "k8s.io/api/core/v1"
 	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"
 	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/template"
 	"github.com/external-secrets/external-secrets/pkg/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(_ 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
@@ -167,14 +52,14 @@ func (r *Reconciler) applyTemplate(ctx context.Context, es *esv1beta1.ExternalSe
 		return err
 		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
 	// apply templates defined in template.templateFrom
-	err = p.MergeTemplateFrom(ctx, es)
+	err = p.MergeTemplateFrom(ctx, es.Namespace, es.Spec.Target.Template)
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf(errFetchTplFrom, err)
 		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
 	// Clean up Labels and Annotations added by the operator
 	// so that it won't leave outdated ones
 	// so that it won't leave outdated ones
-	labelKeys, err := getManagedLabelKeys(secret, es.Name)
+	labelKeys, err := templating.GetManagedLabelKeys(secret, es.Name)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -220,7 +105,7 @@ func setMetadata(secret *v1.Secret, es *esv1beta1.ExternalSecret) error {
 		delete(secret.ObjectMeta.Labels, key)
 		delete(secret.ObjectMeta.Labels, key)
 	}
 	}
 
 
-	annotationKeys, err := getManagedAnnotationKeys(secret, es.Name)
+	annotationKeys, err := templating.GetManagedAnnotationKeys(secret, es.Name)
 	if err != nil {
 	if err != nil {
 		return err
 		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)
 	utils.MergeStringMap(secret.ObjectMeta.Annotations, es.Spec.Target.Template.Metadata.Annotations)
 	return nil
 	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 (
 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 {
 type Reconciler struct {
@@ -153,6 +150,11 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
 
 
 		return ctrl.Result{}, err
 		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)
 	syncedSecrets, err := r.PushSecretToProviders(ctx, secretStores, ps, secret, mgr)
 	if err != nil {
 	if err != nil {
 		if errors.Is(err, locks.ErrConflict) {
 		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
 			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.
 	// if target Secret name is not specified it should use the ExternalSecret name.
 	syncAndDeleteSuccessfully := func(tc *testCase) {
 	syncAndDeleteSuccessfully := func(tc *testCase) {
 		fakeProvider.SetSecretFn = func() error {
 		fakeProvider.SetSecretFn = func() error {
@@ -705,6 +768,7 @@ var _ = Describe("ExternalSecret controller", func() {
 			// this must be optional so we can test faulty es configuration
 			// this must be optional so we can test faulty es configuration
 		},
 		},
 		Entry("should sync", syncSuccessfully),
 		Entry("should sync", syncSuccessfully),
+		Entry("should sync with template", syncSuccessfullyWithTemplate),
 		Entry("should delete if DeletionPolicy=Delete", syncAndDeleteSuccessfully),
 		Entry("should delete if DeletionPolicy=Delete", syncAndDeleteSuccessfully),
 		Entry("should track deletion tasks if Delete fails", failDelete),
 		Entry("should track deletion tasks if Delete fails", failDelete),
 		Entry("should track deleted stores if Delete fails", failDeleteStore),
 		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
+}