Browse Source

Draft: feat: implement template (#69)

* feat: implement template
Moritz Johner 5 years ago
parent
commit
8c8064e0e1

+ 1 - 0
.golangci.yaml

@@ -104,6 +104,7 @@ issues:
         - gosec
         - scopelint
         - unparam
+        - lll
 
     # Ease some gocritic warnings on test files.
     - path: _test\.go

+ 2 - 2
Makefile

@@ -34,7 +34,7 @@ ifeq ($(shell git tag),)
 VERSION := $(shell echo "v0.0.0-$$(git rev-list HEAD --count)-g$$(git describe --dirty --always)" | sed 's/-/./2' | sed 's/-/./2')
 else
 # use tags
-VERSION := $(shell git describe --dirty --always --tags | sed 's/-/./2' | sed 's/-/./2' )
+VERSION := $(shell git describe --dirty --always --tags | sed 's/-/./2' | sed 's/-/./2')
 endif
 
 # ====================================================================================
@@ -78,7 +78,7 @@ check-diff: reviewable
 .PHONY: test
 test: generate ## Run tests
 	@$(INFO) go test unit-tests
-	go test ./... -coverprofile cover.out
+	go test -v ./... -coverprofile cover.out
 	@$(OK) go test unit-tests
 
 .PHONY: build

+ 8 - 0
apis/externalsecrets/v1alpha1/externalsecret_types.go

@@ -54,12 +54,16 @@ type ExternalSecretTemplateMetadata struct {
 }
 
 // ExternalSecretTemplate defines a blueprint for the created Secret resource.
+// we can not use native corev1.Secret, it will have empty ObjectMeta values: https://github.com/kubernetes-sigs/controller-tools/issues/448
 type ExternalSecretTemplate struct {
 	// +optional
 	Type corev1.SecretType `json:"type,omitempty"`
 
 	// +optional
 	Metadata ExternalSecretTemplateMetadata `json:"metadata,omitempty"`
+
+	// +optional
+	Data map[string][]byte `json:"data,omitempty"`
 }
 
 // ExternalSecretTarget defines the Kubernetes Secret to be created
@@ -75,6 +79,10 @@ type ExternalSecretTarget struct {
 	// Defaults to 'Owner'
 	// +optional
 	CreationPolicy ExternalSecretCreationPolicy `json:"creationPolicy,omitempty"`
+
+	// Template defines a blueprint for the created Secret resource.
+	// +optional
+	Template *ExternalSecretTemplate `json:"template,omitempty"`
 }
 
 // ExternalSecretData defines the connection between the Kubernetes Secret key (spec.data.<key>) and the Provider data.

+ 21 - 1
apis/externalsecrets/v1alpha1/zz_generated.deepcopy.go

@@ -229,7 +229,7 @@ func (in *ExternalSecretList) DeepCopyObject() runtime.Object {
 func (in *ExternalSecretSpec) DeepCopyInto(out *ExternalSecretSpec) {
 	*out = *in
 	out.SecretStoreRef = in.SecretStoreRef
-	out.Target = in.Target
+	in.Target.DeepCopyInto(&out.Target)
 	if in.RefreshInterval != nil {
 		in, out := &in.RefreshInterval, &out.RefreshInterval
 		*out = new(v1.Duration)
@@ -299,6 +299,11 @@ func (in *ExternalSecretStatusCondition) DeepCopy() *ExternalSecretStatusConditi
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ExternalSecretTarget) DeepCopyInto(out *ExternalSecretTarget) {
 	*out = *in
+	if in.Template != nil {
+		in, out := &in.Template, &out.Template
+		*out = new(ExternalSecretTemplate)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalSecretTarget.
@@ -315,6 +320,21 @@ func (in *ExternalSecretTarget) DeepCopy() *ExternalSecretTarget {
 func (in *ExternalSecretTemplate) DeepCopyInto(out *ExternalSecretTemplate) {
 	*out = *in
 	in.Metadata.DeepCopyInto(&out.Metadata)
+	if in.Data != nil {
+		in, out := &in.Data, &out.Data
+		*out = make(map[string][]byte, len(*in))
+		for key, val := range *in {
+			var outVal []byte
+			if val == nil {
+				(*out)[key] = nil
+			} else {
+				in, out := &val, &outVal
+				*out = make([]byte, len(*in))
+				copy(*out, *in)
+			}
+			(*out)[key] = outVal
+		}
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalSecretTemplate.

+ 25 - 0
deploy/crds/external-secrets.io_externalsecrets.yaml

@@ -133,6 +133,31 @@ spec:
                       managed This field is immutable Defaults to the .metadata.name
                       of the ExternalSecret resource
                     type: string
+                  template:
+                    description: Template defines a blueprint for the created Secret
+                      resource.
+                    properties:
+                      data:
+                        additionalProperties:
+                          format: byte
+                          type: string
+                        type: object
+                      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
+                      type:
+                        type: string
+                    type: object
                 type: object
             required:
             - secretStoreRef

+ 33 - 3
docs/guides-templating.md

@@ -1,3 +1,33 @@
-!!! note "Not implemented"
-    This is currently **not yet** implemented. See [#28](https://github.com/external-secrets/external-secrets/issues/28)
-    for details. Feel free to contribute.
+With External Secrets Operator you can transform the data from the external secret provider before it is stored as `Kind=Secret`. You can do this with the `Spec.Target.Template`. Each data value is interpreted as a [golang template](https://golang.org/pkg/text/template/).
+
+## Examples
+
+You can use templates to inject your secrets into a configuration file that you mount into your pod:
+``` yaml
+{% include 'multiline-template-external-secret.yaml' %}
+```
+
+You can also use pre-defined functions to extract data from your secrets. Here: extract key/cert from a pkcs12 archive and store it as PEM.
+``` yaml
+{% include 'pkcs12-template-external-secret.yaml' %}
+```
+
+## Helper functions
+We provide a bunch of convenience functions that help you transform your secrets. A secret value is a `[]byte`.
+
+| Function       | Description                                                                | Input                            | Output        |
+| -------------- | -------------------------------------------------------------------------- | -------------------------------- | ------------- |
+| pkcs12key      | extracts the private key from a pkcs12 archive                             | `[]byte`                         | `[]byte`      |
+| pkcs12keyPass  | extracts the private key from a pkcs12 archive using the provided password | password `string`, data `[]byte` | `[]byte`      |
+| pkcs12cert     | extracts the certificate from a pkcs12 archive                             | `[]byte`                         | `[]byte`      |
+| pkcs12certPass | extracts the certificate from a pkcs12 archive using the provided password | password `string`, data `[]byte` | `[]byte`      |
+| pemPrivateKey  | PEM encodes the provided bytes as private key                              | `[]byte`                         | `string`      |
+| pemCertificate | PEM encodes the provided bytes as certificate                              | `[]byte`                         | `string`      |
+| base64decode   | decodes the provided bytes as base64                                       | `[]byte`                         | `[]byte`      |
+| base64encode   | encodes the provided bytes as base64                                       | `[]byte`                         | `[]byte`      |
+| fromJSON       | parses the bytes as JSON so you can access individual properties           | `[]byte`                         | `interface{}` |
+| toJSON         | encodes the provided object as json string                                 | `interface{}`                    | `string`      |
+| toString       | converts bytes to string                                                   | `[]byte`                         | `string`      |
+| toBytes        | converts string to bytes                                                   | `string`                         | `[]byte`      |
+| upper          | converts all characters to their upper case                                | `string`                         | `string`      |
+| lower          | converts all character to their lower case                                 | `string`                         | `string`      |

+ 34 - 0
docs/snippets/multiline-template-external-secret.yaml

@@ -0,0 +1,34 @@
+{% raw %}
+apiVersion: external-secrets.io/v1alpha1
+kind: ExternalSecret
+metadata:
+  name: template
+spec:
+  refreshInterval: 1h
+  secretStoreRef:
+    name: secretstore-sample
+    kind: SecretStore
+  target:
+    name: secret-to-be-created
+    # this is how the Kind=Secret will look like
+    template:
+      type: kubernetes.io/tls
+      data:
+        # multiline string
+        config: |
+          datasources:
+          - name: Graphite
+            type: graphite
+            access: proxy
+            url: http://localhost:8080
+            password: "{{ .password | toString }}" # <-- convert []byte to string
+            user: "{{ .user | toString }}"         # <-- convert []byte to string
+
+  data:
+  - secretKey: user
+    remoteRef:
+      key: /grafana/user
+  - secretKey: password
+    remoteRef:
+      key: /grafana/password
+{% endraw %}

+ 26 - 0
docs/snippets/pkcs12-template-external-secret.yaml

@@ -0,0 +1,26 @@
+{% raw %}
+apiVersion: external-secrets.io/v1alpha1
+kind: ExternalSecret
+metadata:
+  name: template
+spec:
+  refreshInterval: 1h
+  secretStoreRef:
+    name: secretstore-sample
+    kind: SecretStore
+  target:
+    name: secret-to-be-created
+    # this is how the Kind=Secret will look like
+    template:
+      type: kubernetes.io/tls
+      data:
+        tls.crt: "{{ .mysecret | pkcs12cert | pemCertificate }}"
+        tls.key: "{{ .mysecret | pkcs12key | pemPrivateKey }}"
+
+  data:
+  # this is a pkcs12 archive that contains
+  # a cert and a private key
+  - secretKey: mysecret
+    remoteRef:
+      key: example
+{% endraw %}

+ 3 - 1
go.mod

@@ -34,6 +34,7 @@ replace (
 require (
 	github.com/aws/aws-sdk-go v1.38.6
 	github.com/crossplane/crossplane-runtime v0.13.0
+	github.com/fatih/color v1.10.0 // indirect
 	github.com/frankban/quicktest v1.10.0 // indirect
 	github.com/go-logr/logr v0.4.0
 	github.com/gogo/protobuf v1.3.2 // indirect
@@ -57,7 +58,8 @@ require (
 	github.com/prometheus/client_model v0.2.0
 	github.com/stretchr/testify v1.6.1
 	github.com/tidwall/gjson v1.7.4
-	golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect
+	github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
+	golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
 	golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect
 	golang.org/x/oauth2 v0.0.0-20210201163806-010130855d6c // indirect
 	golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf // indirect

+ 5 - 5
go.sum

@@ -140,8 +140,9 @@ github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLi
 github.com/evanphx/json-patch v4.9.0+incompatible h1:kLcOMZeuLAJvL2BPWLMIj5oaZQobrkAqrL+WFZwQses=
 github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
-github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
 github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
+github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
+github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
 github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
 github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
 github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
@@ -529,9 +530,7 @@ github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40T
 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
-github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
 github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
-github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
 github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
@@ -539,7 +538,6 @@ github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkB
 github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
 github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
-github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
@@ -593,6 +591,8 @@ github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijb
 github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
 github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
+github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
+github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
 github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -637,6 +637,7 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
@@ -784,7 +785,6 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
 golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210309074719-68d13333faf2 h1:46ULzRKLh1CwgRq2dC5SlBzEqqNCi8rreOZnNrbqcIY=
 golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

+ 32 - 14
pkg/controllers/externalsecret/externalsecret_controller.go

@@ -35,6 +35,7 @@ import (
 	// Loading registered providers.
 	_ "github.com/external-secrets/external-secrets/pkg/provider/register"
 	schema "github.com/external-secrets/external-secrets/pkg/provider/schema"
+	"github.com/external-secrets/external-secrets/pkg/template"
 	utils "github.com/external-secrets/external-secrets/pkg/utils"
 )
 
@@ -64,13 +65,6 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
 		return ctrl.Result{}, client.IgnoreNotFound(err)
 	}
 
-	secret := &corev1.Secret{
-		ObjectMeta: metav1.ObjectMeta{
-			Name:      externalSecret.Spec.Target.Name,
-			Namespace: externalSecret.Namespace,
-		},
-	}
-
 	store, err := r.getStore(ctx, &externalSecret)
 	if err != nil {
 		log.Error(err, "could not get store reference")
@@ -106,21 +100,24 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
 		syncCallsError.With(syncCallsMetricLabels).Inc()
 		return ctrl.Result{RequeueAfter: requeueAfter}, nil
 	}
-
+	secret := defaultSecret(externalSecret)
 	_, err = ctrl.CreateOrUpdate(ctx, r.Client, secret, func() error {
 		err = controllerutil.SetControllerReference(&externalSecret, &secret.ObjectMeta, r.Scheme)
 		if err != nil {
 			return fmt.Errorf("could not set ExternalSecret controller reference: %w", err)
 		}
-
-		secret.Labels = externalSecret.Labels
-		secret.Annotations = externalSecret.Annotations
-
-		secret.Data, err = r.getProviderSecretData(ctx, secretClient, &externalSecret)
+		data, err := r.getProviderSecretData(ctx, secretClient, &externalSecret)
 		if err != nil {
 			return fmt.Errorf("could not get secret data from provider: %w", err)
 		}
-
+		// overwrite data
+		for k, v := range data {
+			secret.Data[k] = v
+		}
+		err = template.Execute(secret, data)
+		if err != nil {
+			return fmt.Errorf("could not execute template: %w", err)
+		}
 		return nil
 	})
 
@@ -163,6 +160,27 @@ func shouldProcessStore(store esv1alpha1.GenericStore, class string) bool {
 	return false
 }
 
+func defaultSecret(es esv1alpha1.ExternalSecret) *corev1.Secret {
+	secret := &corev1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:        es.Spec.Target.Name,
+			Namespace:   es.Namespace,
+			Labels:      es.Labels,
+			Annotations: es.Annotations,
+		},
+		Data: make(map[string][]byte),
+	}
+
+	if es.Spec.Target.Template != nil {
+		secret.Type = es.Spec.Target.Template.Type
+		secret.Data = es.Spec.Target.Template.Data
+		secret.ObjectMeta.Labels = es.Spec.Target.Template.Metadata.Labels
+		secret.ObjectMeta.Annotations = es.Spec.Target.Template.Metadata.Annotations
+	}
+
+	return secret
+}
+
 func (r *Reconciler) getStore(ctx context.Context, externalSecret *esv1alpha1.ExternalSecret) (esv1alpha1.GenericStore, error) {
 	// TODO: Implement getting ClusterSecretStore
 	var secretStore esv1alpha1.SecretStore

+ 71 - 0
pkg/controllers/externalsecret/externalsecret_controller_test.go

@@ -231,6 +231,77 @@ var _ = Describe("ExternalSecret controller", func() {
 			Expect(syncedSecret.ObjectMeta.Annotations).To(BeEquivalentTo(es.ObjectMeta.Annotations))
 		})
 
+		It("should set the secret value and use the provided secret template", func() {
+			By("creating an ExternalSecret")
+			ctx := context.Background()
+			const targetProp = "targetProperty"
+			const secretVal = "someValue"
+			const templateSecretKey = "tplkey"
+			const templateSecretVal = "{{ .targetProperty | toString | upper }}"
+			es := &esv1alpha1.ExternalSecret{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      ExternalSecretName,
+					Namespace: ExternalSecretNamespace,
+					Labels: map[string]string{
+						"fooobar": "bazz",
+					},
+					Annotations: map[string]string{
+						"hihihih": "hehehe",
+					},
+				},
+				Spec: esv1alpha1.ExternalSecretSpec{
+					SecretStoreRef: esv1alpha1.SecretStoreRef{
+						Name: ExternalSecretStore,
+					},
+					Target: esv1alpha1.ExternalSecretTarget{
+						Name: ExternalSecretTargetSecretName,
+						Template: &esv1alpha1.ExternalSecretTemplate{
+							Metadata: esv1alpha1.ExternalSecretTemplateMetadata{
+								Labels: map[string]string{
+									"foos": "ball",
+								},
+								Annotations: map[string]string{
+									"hihi": "ga",
+								},
+							},
+							Data: map[string][]byte{
+								templateSecretKey: []byte(templateSecretVal),
+							},
+						},
+					},
+					Data: []esv1alpha1.ExternalSecretData{
+						{
+							SecretKey: targetProp,
+							RemoteRef: esv1alpha1.ExternalSecretDataRemoteRef{
+								Key:      "barz",
+								Property: "bang",
+							},
+						},
+					},
+				},
+			}
+
+			fakeProvider.WithGetSecret([]byte(secretVal), nil)
+			Expect(k8sClient.Create(ctx, es)).Should(Succeed())
+			secretLookupKey := types.NamespacedName{
+				Name:      ExternalSecretTargetSecretName,
+				Namespace: ExternalSecretNamespace}
+			syncedSecret := &v1.Secret{}
+			Eventually(func() bool {
+				err := k8sClient.Get(ctx, secretLookupKey, syncedSecret)
+				if err != nil {
+					return false
+				}
+				v1 := syncedSecret.Data[targetProp]
+				v2 := syncedSecret.Data[templateSecretKey]
+				return string(v1) == secretVal && string(v2) == "SOMEVALUE" // templated
+			}, timeout, interval).Should(BeTrue())
+			Expect(syncedSecret.ObjectMeta.Labels).To(BeEquivalentTo(
+				es.Spec.Target.Template.Metadata.Labels))
+			Expect(syncedSecret.ObjectMeta.Annotations).To(BeEquivalentTo(
+				es.Spec.Target.Template.Metadata.Annotations))
+		})
+
 		It("should refresh secret value", func() {
 			ctx := context.Background()
 			const targetProp = "targetProperty"

+ 162 - 0
pkg/template/template.go

@@ -0,0 +1,162 @@
+/*
+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 template
+
+import (
+	"bytes"
+	"encoding/base64"
+	"encoding/json"
+	"encoding/pem"
+	"strings"
+	tpl "text/template"
+
+	"github.com/youmark/pkcs8"
+	"golang.org/x/crypto/pkcs12"
+	corev1 "k8s.io/api/core/v1"
+	ctrl "sigs.k8s.io/controller-runtime"
+)
+
+var tplFuncs = tpl.FuncMap{
+	"pkcs12key":      pkcs12key,
+	"pkcs12keyPass":  pkcs12keyPass,
+	"pkcs12cert":     pkcs12cert,
+	"pkcs12certPass": pkcs12certPass,
+
+	"pemPrivateKey":  pemPrivateKey,
+	"pemCertificate": pemCertificate,
+	"base64decode":   base64decode,
+	"base64encode":   base64encode,
+	"fromJSON":       fromJSON,
+	"toJSON":         toJSON,
+
+	"toString": toString,
+	"toBytes":  toBytes,
+	"upper":    strings.ToUpper,
+	"lower":    strings.ToLower,
+}
+
+var log = ctrl.Log.WithName("template")
+
+// Execute uses an best-effort approach to render the secret data as template.
+func Execute(secret *corev1.Secret, data map[string][]byte) error {
+	for k, v := range secret.Data {
+		t, err := tpl.New(k).
+			Funcs(tplFuncs).
+			Parse(string(v))
+		if err != nil {
+			log.Error(err, "unable to parse template at key", "key", k)
+			continue
+		}
+		buf := bytes.NewBuffer(nil)
+		err = t.Execute(buf, data)
+		if err != nil {
+			log.Error(err, "unable to execute template at key", "key", k)
+			continue
+		}
+		secret.Data[k] = buf.Bytes()
+	}
+	return nil
+}
+
+func pkcs12keyPass(pass string, input []byte) []byte {
+	key, _, err := pkcs12.Decode(input, pass)
+	if err != nil {
+		log.Error(err, "unable to decode pkcs12 with password")
+		return nil
+	}
+	kb, err := pkcs8.ConvertPrivateKeyToPKCS8(key)
+	if err != nil {
+		log.Error(err, "unable to convert pkcs12 private key")
+		return nil
+	}
+	return kb
+}
+
+func pkcs12key(input []byte) []byte {
+	return pkcs12keyPass("", input)
+}
+
+func pkcs12certPass(pass string, input []byte) []byte {
+	_, cert, err := pkcs12.Decode(input, pass)
+	if err != nil {
+		log.Error(err, "unable to decode pkcs12 certificate with password")
+		return nil
+	}
+	return cert.Raw
+}
+
+func pkcs12cert(input []byte) []byte {
+	return pkcs12certPass("", input)
+}
+
+func pemPrivateKey(key []byte) string {
+	buf := bytes.NewBuffer(nil)
+	err := pem.Encode(buf, &pem.Block{Type: "PRIVATE KEY", Bytes: key})
+	if err != nil {
+		log.Error(err, "unable to encode pem private key")
+		return ""
+	}
+	return buf.String()
+}
+
+func pemCertificate(cert []byte) string {
+	buf := bytes.NewBuffer(nil)
+	err := pem.Encode(buf, &pem.Block{Type: "CERTIFICATE", Bytes: cert})
+	if err != nil {
+		log.Error(err, "unable to encode pem certificate")
+		return ""
+	}
+	return buf.String()
+}
+
+func base64decode(in []byte) []byte {
+	out := make([]byte, len(in))
+	l, err := base64.StdEncoding.Decode(out, in)
+	if err != nil {
+		log.Error(err, "unable to encode base64")
+		return []byte("")
+	}
+	return out[:l]
+}
+
+func base64encode(in []byte) []byte {
+	out := make([]byte, base64.StdEncoding.EncodedLen(len(in)))
+	base64.StdEncoding.Encode(out, in)
+	return out
+}
+
+func fromJSON(in []byte) interface{} {
+	var out interface{}
+	err := json.Unmarshal(in, &out)
+	if err != nil {
+		log.Error(err, "unable to unmarshal json")
+	}
+	return out
+}
+
+func toJSON(in interface{}) string {
+	output, err := json.Marshal(in)
+	if err != nil {
+		log.Error(err, "unable to marshal json")
+	}
+	return string(output)
+}
+
+func toString(in []byte) string {
+	return string(in)
+}
+
+func toBytes(in string) []byte {
+	return []byte(in)
+}

File diff suppressed because it is too large
+ 219 - 0
pkg/template/template_test.go