Przeglądaj źródła

Merge pull request #701 from external-secrets/feature/template-string-interface

feat: implement template engine v2
paul-the-alien[bot] 4 lat temu
rodzic
commit
86aedda434
52 zmienionych plików z 1535 dodań i 64 usunięć
  1. 2 2
      Makefile
  2. 13 0
      apis/externalsecrets/v1alpha1/externalsecret_types.go
  3. 6 0
      deploy/crds/external-secrets.io_externalsecrets.yaml
  4. 50 0
      docs/guides-templating-v1.md
  5. 130 28
      docs/guides-templating.md
  6. 25 0
      docs/snippets/jwk-template-v2-external-secret.yaml
  7. 4 0
      docs/snippets/multiline-template-v1-external-secret.yaml
  8. 32 0
      docs/snippets/multiline-template-v2-external-secret.yaml
  9. 0 0
      docs/snippets/pkcs12-template-v1-external-secret.yaml
  10. 19 0
      docs/snippets/pkcs12-template-v2-external-secret.yaml
  11. 0 0
      docs/snippets/template-v1-from-secret.yaml
  12. 41 0
      docs/snippets/template-v2-from-secret.yaml
  13. 36 0
      docs/spec.md
  14. 3 2
      e2e/framework/framework.go
  15. 1 0
      e2e/suite/import.go
  16. 89 0
      e2e/suite/template/provider.go
  17. 101 0
      e2e/suite/template/template.go
  18. 4 2
      go.mod
  19. 10 4
      go.sum
  20. 1 1
      hack/api-docs/Dockerfile
  21. 2 4
      hack/api-docs/Makefile
  22. 3 1
      hack/api-docs/mkdocs.yml
  23. 15 15
      hack/api-docs/requirements.txt
  24. 5 1
      pkg/controllers/externalsecret/externalsecret_controller_template.go
  25. 21 0
      pkg/controllers/externalsecret/externalsecret_controller_test.go
  26. 2 2
      pkg/provider/webhook/webhook.go
  27. 36 0
      pkg/template/engine.go
  28. 0 2
      pkg/template/v1/template.go
  29. 0 0
      pkg/template/v1/template_test.go
  30. 80 0
      pkg/template/v2/_testdata/Makefile
  31. 31 0
      pkg/template/v2/_testdata/chain.pem
  32. 29 0
      pkg/template/v2/_testdata/disjunct-chain.pem
  33. 9 0
      pkg/template/v2/_testdata/disjunct-root-ca.crt
  34. 3 0
      pkg/template/v2/_testdata/disjunct-root-ca.key
  35. BIN
      pkg/template/v2/_testdata/foo-disjunct-nopass.pfx
  36. BIN
      pkg/template/v2/_testdata/foo-multibag-nopass.pfx
  37. BIN
      pkg/template/v2/_testdata/foo-nopass.pfx
  38. BIN
      pkg/template/v2/_testdata/foo-withpass-1234.pfx
  39. 11 0
      pkg/template/v2/_testdata/foo.crt
  40. 5 0
      pkg/template/v2/_testdata/foo.key
  41. 11 0
      pkg/template/v2/_testdata/intermediate-ca.crt
  42. 5 0
      pkg/template/v2/_testdata/intermediate-ca.key
  43. 20 0
      pkg/template/v2/_testdata/intermediate-chain.pem
  44. 9 0
      pkg/template/v2/_testdata/root-ca.crt
  45. 3 0
      pkg/template/v2/_testdata/root-ca.key
  46. 55 0
      pkg/template/v2/jwk.go
  47. 62 0
      pkg/template/v2/pem.go
  48. 141 0
      pkg/template/v2/pem_chain.go
  49. 180 0
      pkg/template/v2/pem_test.go
  50. 109 0
      pkg/template/v2/pkcs12.go
  51. 95 0
      pkg/template/v2/template.go
  52. 26 0
      pkg/template/v2/template_test.go

+ 2 - 2
Makefile

@@ -194,8 +194,8 @@ docs: generate ## Generate docs
 docs.publish: generate ## Generate and deploys docs
 	$(MAKE) -C ./hack/api-docs build.publish
 
-.PHONY: serve-docs
-serve-docs: ## Serve docs
+.PHONY: docs.serve
+docs.serve: ## Serve docs
 	$(MAKE) -C ./hack/api-docs serve
 
 # ====================================================================================

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

@@ -59,6 +59,12 @@ type ExternalSecretTemplate struct {
 	// +optional
 	Type corev1.SecretType `json:"type,omitempty"`
 
+	// EngineVersion specifies the template engine version
+	// that should be used to compile/execute the
+	// template specified in .data and .templateFrom[].
+	// +kubebuilder:default="v1"
+	EngineVersion TemplateEngineVersion `json:"engineVersion,omitempty"`
+
 	// +optional
 	Metadata ExternalSecretTemplateMetadata `json:"metadata,omitempty"`
 
@@ -69,6 +75,13 @@ type ExternalSecretTemplate struct {
 	TemplateFrom []TemplateFrom `json:"templateFrom,omitempty"`
 }
 
+type TemplateEngineVersion string
+
+const (
+	TemplateEngineV1 TemplateEngineVersion = "v1"
+	TemplateEngineV2 TemplateEngineVersion = "v2"
+)
+
 // +kubebuilder:validation:MinProperties=1
 // +kubebuilder:validation:MaxProperties=1
 type TemplateFrom struct {

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

@@ -148,6 +148,12 @@ spec:
                         additionalProperties:
                           type: string
                         type: object
+                      engineVersion:
+                        default: v1
+                        description: EngineVersion specifies the template engine version
+                          that should be used to compile/execute the template specified
+                          in .data and .templateFrom[].
+                        type: string
                       metadata:
                         description: ExternalSecretTemplateMetadata defines metadata
                           fields for the Secret blueprint.

+ 50 - 0
docs/guides-templating-v1.md

@@ -0,0 +1,50 @@
+# Advanced Templating v1
+
+!!! warning
+
+    Templating Engine v1 is **deprecated** and will be removed in the future. Please migrate to engine v2 and take a look at our [upgrade guide](guides-templating.md#migrating-from-v1) for changes.
+
+
+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-v1-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-v1-external-secret.yaml' %}
+```
+
+### TemplateFrom
+
+You do not have to define your templates inline in an ExternalSecret but you can pull `ConfigMaps` or other Secrets that contain a template. Consider the following example:
+
+``` yaml
+{% include 'template-v1-from-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`      |
+| jwkPublicKeyPem | takes an json-serialized JWK as `[]byte` and returns an PEM block of type `PUBLIC KEY` that contains the public key ([see here](https://golang.org/pkg/crypto/x509/#MarshalPKIXPublicKey)) for details | `[]byte`                         | `string`      |
+| jwkPrivateKeyPem | takes an json-serialized JWK as `[]byte` and returns an PEM block of type `PRIVATE KEY` that contains the private key in PKCS #8 format ([see here](https://golang.org/pkg/crypto/x509/#MarshalPKCS8PrivateKey)) for details | `[]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`      |

+ 130 - 28
docs/guides-templating.md

@@ -1,43 +1,145 @@
+# Advanced Templating v2
+
 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' %}
+```yaml
+{% include 'multiline-template-v2-external-secret.yaml' %}
 ```
 
 ### TemplateFrom
 
 You do not have to define your templates inline in an ExternalSecret but you can pull `ConfigMaps` or other Secrets that contain a template. Consider the following example:
 
-``` yaml
-{% include 'template-from-secret.yaml' %}
+```yaml
+{% include 'template-v2-from-secret.yaml' %}
+```
+
+### Extract Keys and Certificates from PKCS#12 Archive
+
+You can use pre-defined functions to extract data from your secrets. Here: extract keys and certificates from a PKCS#12 archive and store it as PEM.
+
+```yaml
+{% include 'pkcs12-template-v2-external-secret.yaml' %}
+```
+
+### Extract from JWK
+
+You can extract the public or private key parts of a JWK and use them as [PKCS#8](https://pkg.go.dev/crypto/x509#ParsePKCS8PrivateKey) private key or PEM-encoded [PKIX](https://pkg.go.dev/crypto/x509#MarshalPKIXPublicKey) public key.
+
+A JWK looks similar to this:
+
+```json
+{
+  "kty": "RSA",
+  "kid": "cc34c0a0-bd5a-4a3c-a50d-a2a7db7643df",
+  "use": "sig",
+  "n": "pjdss...",
+  "e": "AQAB"
+  // ...
+}
+```
+
+And what you want may be a PEM-encoded public or private key portion of it. Take a look at this example on how to transform it into the desired format:
+
+```yaml
+{% include 'jwk-template-v2-external-secret.yaml' %}
+```
+
+### Filter PEM blocks
+
+Consider you have a secret that contains both a certificate and a private key encoded in PEM format and it is your goal to use only the certificate from that secret.
+
+```
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCvxGZOW4IXvGlh
+ . . .
+m8JCpbJXDfSSVxKHgK1Siw4K6pnTsIA2e/Z+Ha2fvtocERjq7VQMAJFaIZSTKo9Q
+JwwY+vj0yxWjyzHUzZB33tg=
+-----END PRIVATE KEY-----
+-----BEGIN CERTIFICATE-----
+MIIDMDCCAhigAwIBAgIQabPaXuZCQaCg+eQAVptGGDANBgkqhkiG9w0BAQsFADAV
+ . . .
+NtFUGA95RGN9s+pl6XY0YARPHf5O76ErC1OZtDTR5RdyQfcM+94gYZsexsXl0aQO
+9YD3Wg==
+-----END CERTIFICATE-----
+
+```
+
+You can achieve that by using the `filterPEM` function to extract a specific type of PEM block from that secret. If multiple blocks of that type (here: `CERTIFICATE`) exist then all of them are returned in the order they are specified.
+```yaml
+{% include 'pem-filter-template-v2-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`      |
-| jwkPublicKeyPem | takes an json-serialized JWK as `[]byte` and returns an PEM block of type `PUBLIC KEY` that contains the public key ([see here](https://golang.org/pkg/crypto/x509/#MarshalPKIXPublicKey)) for details | `[]byte`                         | `string`      |
-| jwkPrivateKeyPem | takes an json-serialized JWK as `[]byte` and returns an PEM block of type `PRIVATE KEY` that contains the private key in PKCS #8 format ([see here](https://golang.org/pkg/crypto/x509/#MarshalPKCS8PrivateKey)) for details | `[]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`      |
+
+!!! info inline end
+
+    Note: we removed `env` and `expandenv` from sprig functions for security reasons.
+
+We provide a couple of convenience functions that help you transform your secrets. This is useful when dealing with PKCS#12 archives or JSON Web Keys (JWK).
+
+In addition to that you can use over 200+ [sprig functions](http://masterminds.github.io/sprig/). If you feel a function is missing or might be valuable feel free to open an issue and submit a [pull request](contributing-process.md#submitting-a-pull-request).
+
+<br/>
+
+| Function       | Description                                                                                                                                                                                               |
+| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| pkcs12key      | Extracts all private keys from a PKCS#12 archive and encodes them in **PKCS#8 PEM** format.                                                                                                               |
+| pkcs12keyPass  | Same as `pkcs12key`. Uses the provided password to decrypt the PKCS#12 archive.                                                                                                                           |
+| pkcs12cert     | Extracts all certificates from a PKCS#12 archive and orders them if possible. If disjunct or multiple leaf certs are provided they are returned as-is. <br/> Sort order: `leaf / intermediate(s) / root`. |
+| pkcs12certPass | Same as `pkcs12cert`. Uses the provided password to decrypt the PKCS#12 archive.                                                                                                                          |
+| filterPEM      | Filters PEM blocks with a specific type from a list of PEM blocks.                                                                                                                                        |
+
+| jwkPublicKeyPem | Takes an json-serialized JWK and returns an PEM block of type `PUBLIC KEY` that contains the public key. [See here](https://golang.org/pkg/crypto/x509/#MarshalPKIXPublicKey) for details. |
+| jwkPrivateKeyPem | Takes an json-serialized JWK as `string` and returns an PEM block of type `PRIVATE KEY` that contains the private key in PKCS #8 format. [See here](https://golang.org/pkg/crypto/x509/#MarshalPKCS8PrivateKey) for details. |
+
+## Migrating from v1
+
+You have to opt-in to use the new engine version by specifying `template.engineVersion=v2`:
+
+```yaml
+apiVersion: external-secrets.io/v1alpha1
+kind: ExternalSecret
+metadata:
+  name: secret
+spec:
+  # ...
+  target:
+    template:
+      engineVersion: v2
+  # ...
+```
+
+The biggest change was that basically all function parameter types were changed from accepting/returning `[]byte` to `string`. This is relevant for you because now you don't need to specify `toString` all the time at the end of a template pipeline.
+
+```yaml
+{% raw %}
+apiVersion: external-secrets.io/v1alpha1
+kind: ExternalSecret
+# ...
+spec:
+  target:
+    template:
+      engineVersion: v2
+      data:
+        # this used to be {{ .foobar | toString }}
+        egg: "new: {{ .foobar }}"
+{% endraw %}
+```
+
+##### Functions removed/replaced
+
+- `base64encode` was renamed to `b64enc`.
+- `base64decode` was renamed to `b64dec`. Any errors that occurr during decoding are silenced.
+- `fromJSON` was renamed to `fromJson`. Any errors that occurr during unmarshalling are silenced.
+- `toJSON` was renamed to `toJson`. Any errors that occurr during marshalling are silenced.
+- `pkcs12key` and `pkcs12keyPass` encode the PKCS#8 key directly into PEM format. There is no need to call `pemPrivateKey` anymore. Also, these functions do extract all private keys from the PKCS#12 archive not just the first one.
+- `pkcs12cert` and `pkcs12certPass` encode the certs directly into PEM format. There is no need to call `pemCertificate` anymore. These functions now **extract all certificates** from the PKCS#12 archive not just the first one.
+- `toString` implementation was replaced by the `sprig` implementation and should be api-compatible.
+- `toBytes` was removed.
+- `pemPrivateKey` was removed. It's now implemented within the `pkcs12*` functions.
+- `pemCertificate` was removed. It's now implemented within the `pkcs12*` functions.

+ 25 - 0
docs/snippets/jwk-template-v2-external-secret.yaml

@@ -0,0 +1,25 @@
+{% raw %}
+apiVersion: external-secrets.io/v1alpha1
+kind: ExternalSecret
+metadata:
+  name: template
+spec:
+  # ...
+  target:
+    template:
+      engineVersion: v2
+      data:
+        # .myjwk is a json-encoded JWK string.
+        #
+        # this template will produce for jwk_pub a PEM encoded public key:
+        # -----BEGIN PUBLIC KEY-----
+        # MIIBI...
+        # ...
+        # ...AQAB
+        # -----END PUBLIC KEY-----
+        jwk_pub: "{{ .myjwk | jwkPublicKeyPem }}"
+        # private key is a pem-encoded PKCS#8 private key
+        jwk_priv: "{{ .myjwk | jwkPrivateKeyPem }}"
+
+
+{% endraw %}

+ 4 - 0
docs/snippets/multiline-template-external-secret.yaml → docs/snippets/multiline-template-v1-external-secret.yaml

@@ -10,6 +10,10 @@ spec:
     kind: SecretStore
   target:
     name: secret-to-be-created
+
+    # v1 is the default version
+    engineVersion: v1
+
     # this is how the Kind=Secret will look like
     template:
       type: kubernetes.io/tls

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

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

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


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

@@ -0,0 +1,19 @@
+{% raw %}
+apiVersion: external-secrets.io/v1alpha1
+kind: ExternalSecret
+metadata:
+  name: template
+spec:
+  # ...
+  target:
+    template:
+      type: kubernetes.io/tls
+      engineVersion: v2
+      data:
+        tls.crt: "{{ .mysecret | pkcs12cert }}"
+        tls.key: "{{ .mysecret | pkcs12key }}"
+
+        # if needed unlock the pkcs12 with the password
+        tls.crt: "{{ .mysecret | pkcs12certPass "my-password" }}"
+
+{% endraw %}

+ 0 - 0
docs/snippets/template-from-secret.yaml → docs/snippets/template-v1-from-secret.yaml


+ 41 - 0
docs/snippets/template-v2-from-secret.yaml

@@ -0,0 +1,41 @@
+{% raw %}
+# define your template in a config map
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: grafana-config-tpl
+data:
+  config.yaml: |
+    datasources:
+      - name: Graphite
+        type: graphite
+        access: proxy
+        url: http://localhost:8080
+        password: "{{ .password }}"
+        user: "{{ .user }}"
+---
+apiVersion: external-secrets.io/v1alpha1
+kind: ExternalSecret
+metadata:
+  name: my-template-example
+spec:
+  # ...
+  target:
+    name: secret-to-be-created
+    template:
+      engineVersion: v2
+      templateFrom:
+      - configMap:
+          # name of the configmap to pull in
+          name: grafana-config-tpl
+          # here you define the keys that should be used as template
+          items:
+          - key: config.yaml
+  data:
+  - secretKey: user
+    remoteRef:
+      key: /grafana/user
+  - secretKey: password
+    remoteRef:
+      key: /grafana/password
+{% endraw %}

+ 36 - 0
docs/spec.md

@@ -1408,6 +1408,21 @@ Kubernetes core/v1.SecretType
 </tr>
 <tr>
 <td>
+<code>engineVersion</code></br>
+<em>
+<a href="#external-secrets.io/v1alpha1.TemplateEngineVersion">
+TemplateEngineVersion
+</a>
+</em>
+</td>
+<td>
+<p>EngineVersion specifies the template engine version
+that should be used to compile/execute the
+template specified in .data and .templateFrom[].</p>
+</td>
+</tr>
+<tr>
+<td>
 <code>metadata</code></br>
 <em>
 <a href="#external-secrets.io/v1alpha1.ExternalSecretTemplateMetadata">
@@ -2666,6 +2681,27 @@ Kubernetes meta/v1.Time
 </tr>
 </tbody>
 </table>
+<h3 id="external-secrets.io/v1alpha1.TemplateEngineVersion">TemplateEngineVersion
+(<code>string</code> alias)</p></h3>
+<p>
+(<em>Appears on:</em>
+<a href="#external-secrets.io/v1alpha1.ExternalSecretTemplate">ExternalSecretTemplate</a>)
+</p>
+<p>
+</p>
+<table>
+<thead>
+<tr>
+<th>Value</th>
+<th>Description</th>
+</tr>
+</thead>
+<tbody><tr><td><p>&#34;v1&#34;</p></td>
+<td></td>
+</tr><tr><td><p>&#34;v2&#34;</p></td>
+<td></td>
+</tr></tbody>
+</table>
 <h3 id="external-secrets.io/v1alpha1.TemplateFrom">TemplateFrom
 </h3>
 <p>

+ 3 - 2
e2e/framework/framework.go

@@ -110,8 +110,9 @@ func (f *Framework) Install(a addon.Addon) {
 
 // Compose helps define multiple testcases with same/different auth methods.
 func Compose(descAppend string, f *Framework, fn func(f *Framework) (string, func(*TestCase)), tweaks ...func(*TestCase)) TableEntry {
-	desc, tfn := fn(f)
-	tweaks = append(tweaks, tfn)
+	// prepend common fn to tweaks
+	desc, cfn := fn(f)
+	tweaks = append([]func(*TestCase){cfn}, tweaks...)
 
 	// need to convert []func to []interface{}
 	ifs := make([]interface{}, len(tweaks))

+ 1 - 0
e2e/suite/import.go

@@ -20,5 +20,6 @@ import (
 	_ "github.com/external-secrets/external-secrets/e2e/suite/aws/secretsmanager"
 	_ "github.com/external-secrets/external-secrets/e2e/suite/azure"
 	_ "github.com/external-secrets/external-secrets/e2e/suite/gcp"
+	_ "github.com/external-secrets/external-secrets/e2e/suite/template"
 	_ "github.com/external-secrets/external-secrets/e2e/suite/vault"
 )

+ 89 - 0
e2e/suite/template/provider.go

@@ -0,0 +1,89 @@
+/*
+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 (
+	"context"
+
+	// nolint
+	. "github.com/onsi/ginkgo/v2"
+
+	// nolint
+	. "github.com/onsi/gomega"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	"github.com/external-secrets/external-secrets/e2e/framework"
+)
+
+type templateProvider struct {
+	framework *framework.Framework
+}
+
+func newProvider(f *framework.Framework) *templateProvider {
+	prov := &templateProvider{
+		framework: f,
+	}
+	BeforeEach(prov.BeforeEach)
+	return prov
+}
+
+func (s *templateProvider) CreateSecret(key, val string) {
+	// noop: this provider implements static key/value pairs
+}
+
+func (s *templateProvider) DeleteSecret(key string) {
+	// noop: this provider implements static key/value pairs
+}
+
+func (s *templateProvider) BeforeEach() {
+	// Create a secret store - change these values to match YAML
+	By("creating a secret store for credentials")
+	secretStore := &esv1alpha1.SecretStore{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      s.framework.Namespace.Name,
+			Namespace: s.framework.Namespace.Name,
+		},
+		Spec: esv1alpha1.SecretStoreSpec{
+			Provider: &esv1alpha1.SecretStoreProvider{
+				Fake: &esv1alpha1.FakeProvider{
+					Data: []esv1alpha1.FakeProviderData{
+						{
+							Key:   "foo",
+							Value: "bar",
+						},
+						{
+							Key:   "baz",
+							Value: "bang",
+						},
+						{
+							Key: "map",
+							ValueMap: map[string]string{
+								"foo": "barmap",
+								"bar": "bangmap",
+							},
+						},
+						{
+							Key:   "json",
+							Value: `{"foo":{"bar":"baz"}}`,
+						},
+					},
+				},
+			},
+		},
+	}
+
+	err := s.framework.CRClient.Create(context.Background(), secretStore)
+	Expect(err).ToNot(HaveOccurred())
+}

+ 101 - 0
e2e/suite/template/template.go

@@ -0,0 +1,101 @@
+/*
+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.
+limitations under the License.
+*/
+package template
+
+import (
+
+	// nolint
+	. "github.com/onsi/ginkgo/v2"
+	v1 "k8s.io/api/core/v1"
+
+	esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	"github.com/external-secrets/external-secrets/e2e/framework"
+)
+
+var _ = Describe("[template]", Label("template"), func() {
+	f := framework.New("eso-template")
+	prov := newProvider(f)
+
+	DescribeTable("sync secrets", framework.TableFunc(f, prov),
+		framework.Compose("template v1", f, genericTemplate, useTemplateV1),
+		framework.Compose("template v2", f, genericTemplate, useTemplateV2),
+	)
+})
+
+// useTemplateV1 specifies a test case which uses the template engine v1.
+func useTemplateV1(tc *framework.TestCase) {
+	tc.ExternalSecret.Spec.Target.Template = &esv1alpha1.ExternalSecretTemplate{
+		EngineVersion: esv1alpha1.TemplateEngineV1,
+		Data: map[string]string{
+			"tplv1": "executed: {{ .singlefoo | toString }}|{{ .singlebaz | toString }}",
+			"other": `{{ .foo | toString }}|{{ .bar | toString }}`,
+		},
+	}
+	tc.ExpectedSecret.Data = map[string][]byte{
+		"tplv1": []byte(`executed: bar|bang`),
+		"other": []byte(`barmap|bangmap`),
+	}
+}
+
+// useTemplateV2 specifies a test case which uses the template engine v2.
+func useTemplateV2(tc *framework.TestCase) {
+	tc.ExternalSecret.Spec.Target.Template = &esv1alpha1.ExternalSecretTemplate{
+		EngineVersion: esv1alpha1.TemplateEngineV2,
+		Data: map[string]string{
+			"tplv2":     "executed: {{ .singlefoo }}|{{ .singlebaz }}",
+			"other":     `{{ .foo }}|{{ .bar }}`,
+			"sprig-str": `{{ .foo | upper }}`,
+			"json-ex":   `{{ $var := .singlejson | fromJson }}{{ $var.foo | toJson }}`,
+		},
+	}
+	tc.ExpectedSecret.Data = map[string][]byte{
+		"tplv2":     []byte(`executed: bar|bang`),
+		"other":     []byte(`barmap|bangmap`),
+		"sprig-str": []byte(`BARMAP`),
+		"json-ex":   []byte(`{"bar":"baz"}`),
+	}
+}
+
+// This case uses template engine v1.
+func genericTemplate(f *framework.Framework) (string, func(*framework.TestCase)) {
+	return "[template] should execute template v1", func(tc *framework.TestCase) {
+		tc.ExpectedSecret = &v1.Secret{
+			Type: v1.SecretTypeOpaque,
+		}
+		tc.ExternalSecret.Spec.Data = []esv1alpha1.ExternalSecretData{
+			{
+				SecretKey: "singlefoo",
+				RemoteRef: esv1alpha1.ExternalSecretDataRemoteRef{
+					Key: "foo",
+				},
+			},
+			{
+				SecretKey: "singlebaz",
+				RemoteRef: esv1alpha1.ExternalSecretDataRemoteRef{
+					Key: "baz",
+				},
+			},
+			{
+				SecretKey: "singlejson",
+				RemoteRef: esv1alpha1.ExternalSecretDataRemoteRef{
+					Key: "json",
+				},
+			},
+		}
+		tc.ExternalSecret.Spec.DataFrom = []esv1alpha1.ExternalSecretDataRemoteRef{
+			{
+				Key: "map",
+			},
+		}
+	}
+}

+ 4 - 2
go.mod

@@ -40,8 +40,7 @@ require (
 	github.com/IBM/go-sdk-core/v5 v5.9.1
 	github.com/IBM/secrets-manager-go-sdk v1.0.31
 	github.com/Masterminds/goutils v1.1.1 // indirect
-	github.com/Masterminds/semver v1.5.0 // indirect
-	github.com/Masterminds/sprig v2.22.0+incompatible
+	github.com/Masterminds/sprig/v3 v3.2.2
 	github.com/PaesslerAG/jsonpath v0.1.1
 	github.com/ahmetb/gen-crd-api-reference-docs v0.3.0
 	github.com/akeylesslabs/akeyless-go-cloud-id v0.3.2
@@ -98,6 +97,7 @@ require (
 	github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect
 	github.com/Azure/go-autorest/logger v0.2.1 // indirect
 	github.com/Azure/go-autorest/tracing v0.6.0 // indirect
+	github.com/Masterminds/semver/v3 v3.1.1 // indirect
 	github.com/PaesslerAG/gval v1.0.0 // indirect
 	github.com/armon/go-metrics v0.3.10 // indirect
 	github.com/armon/go-radix v1.0.0 // indirect
@@ -179,8 +179,10 @@ require (
 	github.com/prometheus/procfs v0.7.3 // indirect
 	github.com/russross/blackfriday/v2 v2.0.1 // indirect
 	github.com/ryanuber/go-glob v1.0.0 // indirect
+	github.com/shopspring/decimal v1.2.0 // indirect
 	github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
 	github.com/sony/gobreaker v0.4.2-0.20210216022020-dd874f9dd33b // indirect
+	github.com/spf13/cast v1.3.1 // indirect
 	github.com/spf13/cobra v1.2.1 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
 	github.com/stretchr/objx v0.2.0 // indirect

+ 10 - 4
go.sum

@@ -93,10 +93,10 @@ github.com/IBM/secrets-manager-go-sdk v1.0.31 h1:KRRyeEvlKkkZb90njgReOrK92+IyS6L
 github.com/IBM/secrets-manager-go-sdk v1.0.31/go.mod h1:0Juj6ER/LpDqJ49nw705MNyXSHsHodgztFdkXz5ttxs=
 github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
 github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
-github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
-github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
-github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
-github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
+github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
+github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
+github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8=
+github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk=
 github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
 github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
@@ -515,11 +515,13 @@ github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKe
 github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 h1:xixZ2bWeofWV68J+x6AzmKuVM/JWCQwkWm6GW/MUR6I=
 github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
 github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw=
 github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
+github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
 github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
 github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
@@ -757,6 +759,8 @@ github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFo
 github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
 github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
+github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
+github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
 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=
@@ -777,6 +781,7 @@ github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B
 github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
 github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
 github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
 github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
 github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
 github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
@@ -896,6 +901,7 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/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-20200414173820-0848c9571904/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-20201217014255-9d1352758620/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=

+ 1 - 1
hack/api-docs/Dockerfile

@@ -23,4 +23,4 @@ RUN apk add -U --no-cache \
     bash \
     gcc \
     diffutils \
-  && pip3 install -r /requirements.txt
+  && pip3 install -r /requirements.txt

+ 2 - 4
hack/api-docs/Makefile

@@ -53,8 +53,7 @@ build: image generate $(SOURCES)
 		--rm \
 		--user $(UID):$(GID) \
 		$(MKDOCS_IMAGE) \
-		/bin/bash -c "cd /repo && git config user.email "docs@external-secrets.io" && git config user.name "Docs" && $(MIKE) deploy --update-aliases -F hack/api-docs/mkdocs.yml $(DOCS_VERSION) $(DOCS_ALIAS);"
-
+		/bin/bash -c "cd /repo && git config user.email "docs@external-secrets.io" && git config user.name "Docs" && $(MIKE) deploy --ignore --update-aliases -F hack/api-docs/mkdocs.yml $(DOCS_VERSION) $(DOCS_ALIAS);"
 .PHONY: build.publish
 build.publish: image generate $(SOURCES)
 	mkdir -p $(GENROOT)
@@ -65,7 +64,6 @@ build.publish: image generate $(SOURCES)
 		--user $(UID):$(GID) \
 		$(MKDOCS_IMAGE) \
 		/bin/bash -c "cd /repo && git config user.email "docs@external-secrets.io" && git config user.name "Docs" && $(MIKE) deploy --update-aliases -p -F hack/api-docs/mkdocs.yml $(DOCS_VERSION) $(DOCS_ALIAS);"
-
 .PHONY: generate
 generate:
 	./generate.sh $(SRCDIR)/spec.md
@@ -86,4 +84,4 @@ serve:
 		-p $(SERVE_BIND_ADDRESS):8000:8000 \
 		--rm \
 		$(MKDOCS_IMAGE) \
-		/bin/bash -c "cd /repo && $(MIKE) serve -F hack/api-docs/mkdocs.yml -a 0.0.0.0:8000"
+		/bin/bash -c "cd /repo && mkdocs serve -f hack/api-docs/mkdocs.yml -a 0.0.0.0:8000"

+ 3 - 1
hack/api-docs/mkdocs.yml

@@ -34,7 +34,9 @@ nav:
   - Guides:
     - Introduction: guides-introduction.md
     - Getting started: guides-getting-started.md
-    - Advanced Templating: guides-templating.md
+    - Advanced Templating:
+        v2: guides-templating.md
+        v1: guides-templating-v1.md
     - Controller Classes: guides-controller-class.md
     - All keys, One secret: guides-all-keys-one-secret.md
     - Common K8S Secret Types: guides-common-k8s-secret-types.md

+ 15 - 15
hack/api-docs/requirements.txt

@@ -1,18 +1,18 @@
-Click==7.0
+Click==8.0.3
 htmlmin==0.1.12
-Jinja2==2.11.1
-jsmin==2.2.2
-livereload==2.6.1
-Markdown==3.2.1
-MarkupSafe==1.1.1
+Jinja2==3.0.3
+jsmin==3.0.1
+livereload==2.6.3
+Markdown==3.3.6
+MarkupSafe==2.0.1
 mkdocs==1.2.3
 mike==1.1.2
-mkdocs-material==8.1.9
-mkdocs-minify-plugin==0.2.1
-pep562==1.0
-Pygments==2.10.0
-pymdown-extensions==9.0
-PyYAML==5.3
-six==1.14.0
-tornado==6.0.3
-mkdocs-macros-plugin==0.4.18
+mkdocs-material==8.1.10
+mkdocs-minify-plugin==0.5.0
+pep562==1.1
+Pygments==2.11.2
+pymdown-extensions==9.1
+PyYAML==6.0
+six==1.16.0
+tornado==6.1
+mkdocs-macros-plugin==0.6.4

+ 5 - 1
pkg/controllers/externalsecret/externalsecret_controller_template.go

@@ -56,7 +56,11 @@ func (r *Reconciler) applyTemplate(ctx context.Context, es *esv1alpha1.ExternalS
 	}
 	r.Log.V(1).Info("found template data", "tpl_data", tplMap)
 
-	err = template.Execute(tplMap, dataMap, secret)
+	execute, err := template.EngineForVersion(es.Spec.Target.Template.EngineVersion)
+	if err != nil {
+		return err
+	}
+	err = execute(tplMap, dataMap, secret)
 	if err != nil {
 		return fmt.Errorf(errExecTpl, err)
 	}

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

@@ -433,6 +433,9 @@ var _ = Describe("ExternalSecret controller", func() {
 					"hihi": "ga",
 				},
 			},
+			// We do not specify the engine version
+			// it should default to v1 for alpha1
+			// EngineVersion: esv1alpha1.TemplateEngineV1,
 			Type: v1.SecretTypeOpaque,
 			Data: map[string]string{
 				targetProp:   targetPropObj,
@@ -453,6 +456,23 @@ var _ = Describe("ExternalSecret controller", func() {
 		}
 	}
 
+	// when using a v2 template it should use the v2 engine version
+	syncWithTemplateV2 := func(tc *testCase) {
+		const secretVal = "someValue"
+		tc.externalSecret.Spec.Target.Template = &esv1alpha1.ExternalSecretTemplate{
+			Type:          v1.SecretTypeOpaque,
+			EngineVersion: esv1alpha1.TemplateEngineV2,
+			Data: map[string]string{
+				targetProp: "{{ .targetProperty | upper }} was templated",
+			},
+		}
+		fakeProvider.WithGetSecret([]byte(secretVal), nil)
+		tc.checkSecret = func(es *esv1alpha1.ExternalSecret, secret *v1.Secret) {
+			// check values
+			Expect(string(secret.Data[targetProp])).To(Equal(expectedSecretVal))
+		}
+	}
+
 	// secret should be synced with correct value precedence:
 	// * template
 	// * templateFrom
@@ -1046,6 +1066,7 @@ var _ = Describe("ExternalSecret controller", func() {
 		Entry("should not resolve conflicts with creationPolicy=Merge", mergeWithConflict),
 		Entry("should not update unchanged secret using creationPolicy=Merge", mergeWithSecretNoChange),
 		Entry("should sync with template", syncWithTemplate),
+		Entry("should sync with template engine v2", syncWithTemplateV2),
 		Entry("should sync template with correct value precedence", syncWithTemplatePrecedence),
 		Entry("should refresh secret from template", refreshWithTemplate),
 		Entry("should be able to use only metadata from template", onlyMetadataFromTemplate),

+ 2 - 2
pkg/provider/webhook/webhook.go

@@ -26,7 +26,7 @@ import (
 	"strings"
 	tpl "text/template"
 
-	"github.com/Masterminds/sprig"
+	"github.com/Masterminds/sprig/v3"
 	"github.com/PaesslerAG/jsonpath"
 	"gopkg.in/yaml.v3"
 	corev1 "k8s.io/api/core/v1"
@@ -36,7 +36,7 @@ import (
 	esmeta "github.com/external-secrets/external-secrets/apis/meta/v1"
 	"github.com/external-secrets/external-secrets/pkg/provider"
 	"github.com/external-secrets/external-secrets/pkg/provider/schema"
-	"github.com/external-secrets/external-secrets/pkg/template"
+	"github.com/external-secrets/external-secrets/pkg/template/v2"
 )
 
 // Provider satisfies the provider interface.

+ 36 - 0
pkg/template/engine.go

@@ -0,0 +1,36 @@
+/*
+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.
+limitations under the License.
+*/
+package template
+
+import (
+	corev1 "k8s.io/api/core/v1"
+
+	esapi "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	v1 "github.com/external-secrets/external-secrets/pkg/template/v1"
+	v2 "github.com/external-secrets/external-secrets/pkg/template/v2"
+)
+
+type ExecFunc func(tpl, data map[string][]byte, secret *corev1.Secret) error
+
+func EngineForVersion(version esapi.TemplateEngineVersion) (ExecFunc, error) {
+	switch version {
+	case esapi.TemplateEngineV1:
+		return v1.Execute, nil
+	case esapi.TemplateEngineV2:
+		return v2.Execute, nil
+	}
+
+	// in case we run with a old v1alpha1 CRD
+	// we must return v1 as default
+	return v1.Execute, nil
+}

+ 0 - 2
pkg/template/template.go → pkg/template/v1/template.go

@@ -2,9 +2,7 @@
 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.

+ 0 - 0
pkg/template/template_test.go → pkg/template/v1/template_test.go


+ 80 - 0
pkg/template/v2/_testdata/Makefile

@@ -0,0 +1,80 @@
+# prerequisite:
+# install step cli
+# from: https://github.com/smallstep/cli
+
+all: ca disjunct-ca intermediate leaf  \
+	pkcs12-nopass pkcs12-disjunct pkcs12-multibag pkcs12-withpass-1234
+
+clean:
+	rm *.{pfx,crt,key,pem}
+
+ca:
+	step certificate create root-ca \
+		root-ca.crt root-ca.key \
+		--profile root-ca --kty OKP --curve Ed25519 \
+		--no-password --insecure -f
+
+disjunct-ca:
+	step certificate create disjunct-root-ca \
+		disjunct-root-ca.crt disjunct-root-ca.key \
+		--profile root-ca --kty OKP --curve Ed25519 \
+		--no-password --insecure -f
+
+intermediate:
+	step certificate create intermediate-ca \
+		intermediate-ca.crt intermediate-ca.key \
+            --profile intermediate-ca \
+			--ca ./root-ca.crt \
+			--ca-key ./root-ca.key \
+			--kty EC --curve P-256 \
+			--no-password --insecure -f
+
+leaf:
+	step certificate create foo \
+		foo.crt foo.key --profile leaf \
+		--ca ./intermediate-ca.crt \
+		--ca-key ./intermediate-ca.key \
+		--no-password --insecure -f
+
+pkcs12-nopass: ca intermediate leaf
+	# deliberately in wrong order
+	cat foo.crt root-ca.crt intermediate-ca.crt > chain.pem
+
+	# create pkcs12
+	openssl pkcs12 -export \
+		-in chain.pem \
+		-inkey foo.key \
+		-out foo-nopass.pfx \
+		-password pass:
+
+pkcs12-disjunct: ca intermediate disjunct-ca leaf
+	cat root-ca.crt intermediate-ca.crt disjunct-root-ca.crt > disjunct-chain.pem
+
+	openssl pkcs12 -export \
+		-in foo.crt \
+		-certfile disjunct-chain.pem \
+		-inkey foo.key \
+		-out foo-disjunct-nopass.pfx \
+		-password pass:
+
+pkcs12-multibag: ca intermediate leaf
+	# deliberately in wrong order, we're missing the leaf cert here
+	cat root-ca.crt intermediate-ca.crt > intermediate-chain.pem
+
+	openssl pkcs12 -export \
+		-in foo.crt \
+		-certfile intermediate-chain.pem \
+		-inkey foo.key \
+		-out foo-multibag-nopass.pfx \
+		-password pass:
+
+pkcs12-withpass-1234: ca intermediate leaf
+	# deliberately in the wrong order
+	cat foo.crt root-ca.crt intermediate-ca.crt > chain.pem
+
+	# create pkcs12
+	openssl pkcs12 -export \
+		-in chain.pem \
+		-inkey foo.key \
+		-out foo-withpass-1234.pfx \
+		-password pass:1234

+ 31 - 0
pkg/template/v2/_testdata/chain.pem

@@ -0,0 +1,31 @@
+-----BEGIN CERTIFICATE-----
+MIIBqjCCAU+gAwIBAgIRAPnGGsBUMbZhmh5QdnYdBmUwCgYIKoZIzj0EAwIwGjEY
+MBYGA1UEAxMPaW50ZXJtZWRpYXRlLWNhMB4XDTIyMDIwOTEwMjUzMVoXDTIyMDIx
+MDEwMjUzMVowDjEMMAoGA1UEAxMDZm9vMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcD
+QgAEqnxdeInykx8JZsLi13rZLekoG2cosQ3F+2InVNy7hCQ7soMqdaJsGQ6LFtov
+ogUFtOOTRWrunblqNWGZsowHbKOBgTB/MA4GA1UdDwEB/wQEAwIHgDAdBgNVHSUE
+FjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwHQYDVR0OBBYEFLtundVbuKd73OWzo6SY
+by0Ajeb2MB8GA1UdIwQYMBaAFCLg80J/bZBbOd+Y8+V94l5xM2zEMA4GA1UdEQQH
+MAWCA2ZvbzAKBggqhkjOPQQDAgNJADBGAiEA4K4SbVNqrEtl7RfwBfJFMnWI+X8D
+zMPMc4Xqzp2qTxcCIQDsySgtiakypZfWakpB49zJph0kLwGK8xhWvGMUw1N1/w==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIBJzCB2qADAgECAhEArvunrLoYXTmwMROkmbAlBTAFBgMrZXAwEjEQMA4GA1UE
+AxMHcm9vdC1jYTAeFw0yMjAyMDkxMDI1MzBaFw0zMjAyMDcxMDI1MzBaMBIxEDAO
+BgNVBAMTB3Jvb3QtY2EwKjAFBgMrZXADIQDSw5uQ1io+jcKevCH0sl+tGTB6/BQs
+Bu84ibw13QoP36NFMEMwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8C
+AQEwHQYDVR0OBBYEFGtn1GnNjmd6cxOhMnVau1L8IkGtMAUGAytlcANBAOHSAS4z
+/6ctcvRwlGr9Hyt7vVLROImD2t3rFdDDHLLL1znikK3JZvVbETyMFOMbOMQS33C/
+4FtLGenZFXySjQw=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIBgDCCATKgAwIBAgIRAOzjpCdp42oW5MoccLpRXpAwBQYDK2VwMBIxEDAOBgNV
+BAMTB3Jvb3QtY2EwHhcNMjIwMjA5MTAyNTMxWhcNMzIwMjA3MTAyNTMxWjAaMRgw
+FgYDVQQDEw9pbnRlcm1lZGlhdGUtY2EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC
+AATekdyX6cZe0Ajmme363TQoWnrQwXnARzeWEf4FRQE8BGWgf8z7wljjpb4M4S4f
++CJAYYY/6x38UnlsxXEeBTofo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/
+BAgwBgEB/wIBADAdBgNVHQ4EFgQUIuDzQn9tkFs535jz5X3iXnEzbMQwHwYDVR0j
+BBgwFoAUa2fUac2OZ3pzE6EydVq7UvwiQa0wBQYDK2VwA0EA4gntaGs/3ME6q1y9
+gO4ntri2qwoC25l3q7q9BiFBmeBmvS6I1w9HCZHtB3JnVC/IYDTCYDNTbpGWEOjl
+aCKLCA==
+-----END CERTIFICATE-----

+ 29 - 0
pkg/template/v2/_testdata/disjunct-chain.pem

@@ -0,0 +1,29 @@
+-----BEGIN CERTIFICATE-----
+MIIBJzCB2qADAgECAhEArvunrLoYXTmwMROkmbAlBTAFBgMrZXAwEjEQMA4GA1UE
+AxMHcm9vdC1jYTAeFw0yMjAyMDkxMDI1MzBaFw0zMjAyMDcxMDI1MzBaMBIxEDAO
+BgNVBAMTB3Jvb3QtY2EwKjAFBgMrZXADIQDSw5uQ1io+jcKevCH0sl+tGTB6/BQs
+Bu84ibw13QoP36NFMEMwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8C
+AQEwHQYDVR0OBBYEFGtn1GnNjmd6cxOhMnVau1L8IkGtMAUGAytlcANBAOHSAS4z
+/6ctcvRwlGr9Hyt7vVLROImD2t3rFdDDHLLL1znikK3JZvVbETyMFOMbOMQS33C/
+4FtLGenZFXySjQw=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIBgDCCATKgAwIBAgIRAOzjpCdp42oW5MoccLpRXpAwBQYDK2VwMBIxEDAOBgNV
+BAMTB3Jvb3QtY2EwHhcNMjIwMjA5MTAyNTMxWhcNMzIwMjA3MTAyNTMxWjAaMRgw
+FgYDVQQDEw9pbnRlcm1lZGlhdGUtY2EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC
+AATekdyX6cZe0Ajmme363TQoWnrQwXnARzeWEf4FRQE8BGWgf8z7wljjpb4M4S4f
++CJAYYY/6x38UnlsxXEeBTofo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/
+BAgwBgEB/wIBADAdBgNVHQ4EFgQUIuDzQn9tkFs535jz5X3iXnEzbMQwHwYDVR0j
+BBgwFoAUa2fUac2OZ3pzE6EydVq7UvwiQa0wBQYDK2VwA0EA4gntaGs/3ME6q1y9
+gO4ntri2qwoC25l3q7q9BiFBmeBmvS6I1w9HCZHtB3JnVC/IYDTCYDNTbpGWEOjl
+aCKLCA==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIBOTCB7KADAgECAhEA0cArEY0s5JoP8JfqIDv2BTAFBgMrZXAwGzEZMBcGA1UE
+AxMQZGlzanVuY3Qtcm9vdC1jYTAeFw0yMjAyMDkxMDI1MzBaFw0zMjAyMDcxMDI1
+MzBaMBsxGTAXBgNVBAMTEGRpc2p1bmN0LXJvb3QtY2EwKjAFBgMrZXADIQAJER3w
+QFZH1DBmxZm9IkaZ5noangcg/CYNP+GtcyQPL6NFMEMwDgYDVR0PAQH/BAQDAgEG
+MBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFMddJG4lWXTN/x9InsiJUTxu
+d4+HMAUGAytlcANBABcyLV4om9LPV0TGDf0jiM+JxH1R+ATvAE8FHDtd8L66BrzA
+Id656nPz3fz9ZMB9VZr7iGcghXYlTHxu6NkQEgA=
+-----END CERTIFICATE-----

+ 9 - 0
pkg/template/v2/_testdata/disjunct-root-ca.crt

@@ -0,0 +1,9 @@
+-----BEGIN CERTIFICATE-----
+MIIBOTCB7KADAgECAhEA0cArEY0s5JoP8JfqIDv2BTAFBgMrZXAwGzEZMBcGA1UE
+AxMQZGlzanVuY3Qtcm9vdC1jYTAeFw0yMjAyMDkxMDI1MzBaFw0zMjAyMDcxMDI1
+MzBaMBsxGTAXBgNVBAMTEGRpc2p1bmN0LXJvb3QtY2EwKjAFBgMrZXADIQAJER3w
+QFZH1DBmxZm9IkaZ5noangcg/CYNP+GtcyQPL6NFMEMwDgYDVR0PAQH/BAQDAgEG
+MBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFMddJG4lWXTN/x9InsiJUTxu
+d4+HMAUGAytlcANBABcyLV4om9LPV0TGDf0jiM+JxH1R+ATvAE8FHDtd8L66BrzA
+Id656nPz3fz9ZMB9VZr7iGcghXYlTHxu6NkQEgA=
+-----END CERTIFICATE-----

+ 3 - 0
pkg/template/v2/_testdata/disjunct-root-ca.key

@@ -0,0 +1,3 @@
+-----BEGIN PRIVATE KEY-----
+MC4CAQAwBQYDK2VwBCIEIDWjPU0WTbgryudXkq5qduRP5utcq8yEsmYPizQ0J1vW
+-----END PRIVATE KEY-----

BIN
pkg/template/v2/_testdata/foo-disjunct-nopass.pfx


BIN
pkg/template/v2/_testdata/foo-multibag-nopass.pfx


BIN
pkg/template/v2/_testdata/foo-nopass.pfx


BIN
pkg/template/v2/_testdata/foo-withpass-1234.pfx


+ 11 - 0
pkg/template/v2/_testdata/foo.crt

@@ -0,0 +1,11 @@
+-----BEGIN CERTIFICATE-----
+MIIBqjCCAU+gAwIBAgIRAPnGGsBUMbZhmh5QdnYdBmUwCgYIKoZIzj0EAwIwGjEY
+MBYGA1UEAxMPaW50ZXJtZWRpYXRlLWNhMB4XDTIyMDIwOTEwMjUzMVoXDTIyMDIx
+MDEwMjUzMVowDjEMMAoGA1UEAxMDZm9vMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcD
+QgAEqnxdeInykx8JZsLi13rZLekoG2cosQ3F+2InVNy7hCQ7soMqdaJsGQ6LFtov
+ogUFtOOTRWrunblqNWGZsowHbKOBgTB/MA4GA1UdDwEB/wQEAwIHgDAdBgNVHSUE
+FjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwHQYDVR0OBBYEFLtundVbuKd73OWzo6SY
+by0Ajeb2MB8GA1UdIwQYMBaAFCLg80J/bZBbOd+Y8+V94l5xM2zEMA4GA1UdEQQH
+MAWCA2ZvbzAKBggqhkjOPQQDAgNJADBGAiEA4K4SbVNqrEtl7RfwBfJFMnWI+X8D
+zMPMc4Xqzp2qTxcCIQDsySgtiakypZfWakpB49zJph0kLwGK8xhWvGMUw1N1/w==
+-----END CERTIFICATE-----

+ 5 - 0
pkg/template/v2/_testdata/foo.key

@@ -0,0 +1,5 @@
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIMuAjEwBeXznOjx3V7viagAznflfL+p64CXkm++xlXhkoAoGCCqGSM49
+AwEHoUQDQgAEqnxdeInykx8JZsLi13rZLekoG2cosQ3F+2InVNy7hCQ7soMqdaJs
+GQ6LFtovogUFtOOTRWrunblqNWGZsowHbA==
+-----END EC PRIVATE KEY-----

+ 11 - 0
pkg/template/v2/_testdata/intermediate-ca.crt

@@ -0,0 +1,11 @@
+-----BEGIN CERTIFICATE-----
+MIIBgDCCATKgAwIBAgIRAOzjpCdp42oW5MoccLpRXpAwBQYDK2VwMBIxEDAOBgNV
+BAMTB3Jvb3QtY2EwHhcNMjIwMjA5MTAyNTMxWhcNMzIwMjA3MTAyNTMxWjAaMRgw
+FgYDVQQDEw9pbnRlcm1lZGlhdGUtY2EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC
+AATekdyX6cZe0Ajmme363TQoWnrQwXnARzeWEf4FRQE8BGWgf8z7wljjpb4M4S4f
++CJAYYY/6x38UnlsxXEeBTofo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/
+BAgwBgEB/wIBADAdBgNVHQ4EFgQUIuDzQn9tkFs535jz5X3iXnEzbMQwHwYDVR0j
+BBgwFoAUa2fUac2OZ3pzE6EydVq7UvwiQa0wBQYDK2VwA0EA4gntaGs/3ME6q1y9
+gO4ntri2qwoC25l3q7q9BiFBmeBmvS6I1w9HCZHtB3JnVC/IYDTCYDNTbpGWEOjl
+aCKLCA==
+-----END CERTIFICATE-----

+ 5 - 0
pkg/template/v2/_testdata/intermediate-ca.key

@@ -0,0 +1,5 @@
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIIYmlQRczt7PR7VN9jjKf5MGcgKHgohcBbrPbikYVWNqoAoGCCqGSM49
+AwEHoUQDQgAE3pHcl+nGXtAI5pnt+t00KFp60MF5wEc3lhH+BUUBPARloH/M+8JY
+46W+DOEuH/giQGGGP+sd/FJ5bMVxHgU6Hw==
+-----END EC PRIVATE KEY-----

+ 20 - 0
pkg/template/v2/_testdata/intermediate-chain.pem

@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIBJzCB2qADAgECAhEArvunrLoYXTmwMROkmbAlBTAFBgMrZXAwEjEQMA4GA1UE
+AxMHcm9vdC1jYTAeFw0yMjAyMDkxMDI1MzBaFw0zMjAyMDcxMDI1MzBaMBIxEDAO
+BgNVBAMTB3Jvb3QtY2EwKjAFBgMrZXADIQDSw5uQ1io+jcKevCH0sl+tGTB6/BQs
+Bu84ibw13QoP36NFMEMwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8C
+AQEwHQYDVR0OBBYEFGtn1GnNjmd6cxOhMnVau1L8IkGtMAUGAytlcANBAOHSAS4z
+/6ctcvRwlGr9Hyt7vVLROImD2t3rFdDDHLLL1znikK3JZvVbETyMFOMbOMQS33C/
+4FtLGenZFXySjQw=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIBgDCCATKgAwIBAgIRAOzjpCdp42oW5MoccLpRXpAwBQYDK2VwMBIxEDAOBgNV
+BAMTB3Jvb3QtY2EwHhcNMjIwMjA5MTAyNTMxWhcNMzIwMjA3MTAyNTMxWjAaMRgw
+FgYDVQQDEw9pbnRlcm1lZGlhdGUtY2EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC
+AATekdyX6cZe0Ajmme363TQoWnrQwXnARzeWEf4FRQE8BGWgf8z7wljjpb4M4S4f
++CJAYYY/6x38UnlsxXEeBTofo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/
+BAgwBgEB/wIBADAdBgNVHQ4EFgQUIuDzQn9tkFs535jz5X3iXnEzbMQwHwYDVR0j
+BBgwFoAUa2fUac2OZ3pzE6EydVq7UvwiQa0wBQYDK2VwA0EA4gntaGs/3ME6q1y9
+gO4ntri2qwoC25l3q7q9BiFBmeBmvS6I1w9HCZHtB3JnVC/IYDTCYDNTbpGWEOjl
+aCKLCA==
+-----END CERTIFICATE-----

+ 9 - 0
pkg/template/v2/_testdata/root-ca.crt

@@ -0,0 +1,9 @@
+-----BEGIN CERTIFICATE-----
+MIIBJzCB2qADAgECAhEArvunrLoYXTmwMROkmbAlBTAFBgMrZXAwEjEQMA4GA1UE
+AxMHcm9vdC1jYTAeFw0yMjAyMDkxMDI1MzBaFw0zMjAyMDcxMDI1MzBaMBIxEDAO
+BgNVBAMTB3Jvb3QtY2EwKjAFBgMrZXADIQDSw5uQ1io+jcKevCH0sl+tGTB6/BQs
+Bu84ibw13QoP36NFMEMwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8C
+AQEwHQYDVR0OBBYEFGtn1GnNjmd6cxOhMnVau1L8IkGtMAUGAytlcANBAOHSAS4z
+/6ctcvRwlGr9Hyt7vVLROImD2t3rFdDDHLLL1znikK3JZvVbETyMFOMbOMQS33C/
+4FtLGenZFXySjQw=
+-----END CERTIFICATE-----

+ 3 - 0
pkg/template/v2/_testdata/root-ca.key

@@ -0,0 +1,3 @@
+-----BEGIN PRIVATE KEY-----
+MC4CAQAwBQYDK2VwBCIEICJ7FAxZXElbLQe/yrvr+ZqYQHKf9oGtzsasBZ8a32nk
+-----END PRIVATE KEY-----

+ 55 - 0
pkg/template/v2/jwk.go

@@ -0,0 +1,55 @@
+/*
+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 (
+	"crypto/x509"
+
+	"github.com/lestrrat-go/jwx/jwk"
+)
+
+func jwkPublicKeyPem(jwkjson string) (string, error) {
+	k, err := jwk.ParseKey([]byte(jwkjson))
+	if err != nil {
+		return "", err
+	}
+	var rawkey interface{}
+	err = k.Raw(&rawkey)
+	if err != nil {
+		return "", err
+	}
+	mpk, err := x509.MarshalPKIXPublicKey(rawkey)
+	if err != nil {
+		return "", err
+	}
+	return pemEncode(string(mpk), "PUBLIC KEY")
+}
+
+func jwkPrivateKeyPem(jwkjson string) (string, error) {
+	k, err := jwk.ParseKey([]byte(jwkjson))
+	if err != nil {
+		return "", err
+	}
+	var mpk []byte
+	var pk interface{}
+	err = k.Raw(&pk)
+	if err != nil {
+		return "", err
+	}
+	mpk, err = x509.MarshalPKCS8PrivateKey(pk)
+	if err != nil {
+		return "", err
+	}
+	return pemEncode(string(mpk), "PRIVATE KEY")
+}

+ 62 - 0
pkg/template/v2/pem.go

@@ -0,0 +1,62 @@
+/*
+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/pem"
+	"errors"
+	"strings"
+)
+
+const (
+	errJunk = "error filtering pem: found junk"
+)
+
+func filterPEM(pemType, input string) (string, error) {
+	data := []byte(input)
+	var blocks []byte
+	var block *pem.Block
+	var rest []byte
+	for {
+		block, rest = pem.Decode(data)
+		data = rest
+
+		if block == nil {
+			break
+		}
+		if !strings.EqualFold(block.Type, pemType) {
+			continue
+		}
+
+		var buf bytes.Buffer
+		err := pem.Encode(&buf, block)
+		if err != nil {
+			return "", err
+		}
+		blocks = append(blocks, buf.Bytes()...)
+	}
+
+	if len(blocks) == 0 && len(rest) != 0 {
+		return "", errors.New(errJunk)
+	}
+
+	return string(blocks), nil
+}
+
+func pemEncode(thing, kind string) (string, error) {
+	buf := bytes.NewBuffer(nil)
+	err := pem.Encode(buf, &pem.Block{Type: kind, Bytes: []byte(thing)})
+	return buf.String(), err
+}

+ 141 - 0
pkg/template/v2/pem_chain.go

@@ -0,0 +1,141 @@
+/*
+MIT License
+
+Copyright (c) Microsoft Corporation.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE
+
+Original Author: Anish Ramasekar https://github.com/aramase
+In: https://github.com/Azure/secrets-store-csi-driver-provider-azure/pull/332
+*/
+package template
+
+import (
+	"crypto/x509"
+	"encoding/pem"
+	"fmt"
+)
+
+const (
+	errNilCert           = "certificate is nil"
+	errFoundDisjunctCert = "found multiple leaf or disjunct certificates"
+	errNoLeafFound       = "no leaf certificate found"
+	errChainCycle        = "constructing chain resulted in cycle"
+)
+
+type node struct {
+	cert     *x509.Certificate
+	parent   *node
+	isParent bool
+}
+
+func fetchCertChains(data []byte) ([]byte, error) {
+	var newCertChain []*x509.Certificate
+	var pemData []byte
+	nodes, err := pemToNodes(data)
+	if err != nil {
+		return nil, err
+	}
+
+	// at the end of this computation, the output will be a single linked list
+	// the tail of the list will be the root node (which has no parents)
+	// the head of the list will be the leaf node (whose parent will be intermediate certs)
+	// (head) leaf -> intermediates -> root (tail)
+	for i := range nodes {
+		for j := range nodes {
+			// ignore same node to prevent generating a cycle
+			if i == j {
+				continue
+			}
+			// if ith node AuthorityKeyId is same as jth node SubjectKeyId, jth node was used
+			// to sign the ith certificate
+			if string(nodes[i].cert.AuthorityKeyId) == string(nodes[j].cert.SubjectKeyId) {
+				nodes[j].isParent = true
+				nodes[i].parent = nodes[j]
+				break
+			}
+		}
+	}
+
+	var foundLeaf bool
+	var leaf *node
+	for i := range nodes {
+		if !nodes[i].isParent {
+			if foundLeaf {
+				return nil, fmt.Errorf(errFoundDisjunctCert)
+			}
+			// this is the leaf node as it's not a parent for any other node
+			leaf = nodes[i]
+			foundLeaf = true
+		}
+	}
+
+	if leaf == nil {
+		return nil, fmt.Errorf(errNoLeafFound)
+	}
+
+	processedNodes := 0
+	// iterate through the directed list and append the nodes to new cert chain
+	for leaf != nil {
+		processedNodes++
+		// ensure we aren't stuck in a cyclic loop
+		if processedNodes > len(nodes) {
+			return pemData, fmt.Errorf(errChainCycle)
+		}
+		newCertChain = append(newCertChain, leaf.cert)
+		leaf = leaf.parent
+	}
+
+	for _, cert := range newCertChain {
+		b := &pem.Block{
+			Type:  pemTypeCertificate,
+			Bytes: cert.Raw,
+		}
+		pemData = append(pemData, pem.EncodeToMemory(b)...)
+	}
+	return pemData, nil
+}
+
+func pemToNodes(data []byte) ([]*node, error) {
+	nodes := make([]*node, 0)
+	for {
+		// decode pem to der first
+		block, rest := pem.Decode(data)
+		data = rest
+
+		if block == nil {
+			break
+		}
+		cert, err := x509.ParseCertificate(block.Bytes)
+		if err != nil {
+			return nil, err
+		}
+		// this should not be the case because ParseCertificate should return a non nil
+		// certificate when there is no error.
+		if cert == nil {
+			return nil, fmt.Errorf(errNilCert)
+		}
+		nodes = append(nodes, &node{
+			cert:     cert,
+			parent:   nil,
+			isParent: false,
+		})
+	}
+	return nodes, nil
+}

+ 180 - 0
pkg/template/v2/pem_test.go

@@ -0,0 +1,180 @@
+/*
+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 "testing"
+
+const (
+	certData = `-----BEGIN CERTIFICATE-----
+MIIDHTCCAgWgAwIBAgIRAKC4yxy9QGocND+6avTf7BgwDQYJKoZIhvcNAQELBQAw
+EjEQMA4GA1UEChMHQWNtZSBDbzAeFw0yMTAzMjAyMDA4MDhaFw0yMTAzMjAyMDM4
+MDhaMBIxEDAOBgNVBAoTB0FjbWUgQ28wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
+ggEKAoIBAQC3o6/JdZEqNbqNRkopHhJtJG5c4qS5d0tQ/kZYpfD/v/izAYum4Nzj
+aG15owr92/11W0pxPUliRLti3y6iScTs+ofm2D7p4UXj/Fnho/2xoWSOoWAodgvW
+Y8jh8A0LQALZiV/9QsrJdXZdS47DYZLsQ3z9yFC/CdXkg1l7AQ3fIVGKdrQBr9kE
+1gEDqnKfRxXI8DEQKXr+CKPUwCAytegmy0SHp53zNAvY+kopHytzmJpXLoEhxq4e
+ugHe52vXHdh/HJ9VjNp0xOH1waAgAGxHlltCW0PVd5AJ0SXROBS/a3V9sZCbCrJa
+YOOonQSEswveSv6PcG9AHvpNPot2Xs6hAgMBAAGjbjBsMA4GA1UdDwEB/wQEAwIC
+pDATBgNVHSUEDDAKBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW
+BBR00805mrpoonp95RmC3B6oLl+cGTAVBgNVHREEDjAMggpnb29ibGUuY29tMA0G
+CSqGSIb3DQEBCwUAA4IBAQAipc1b6JrEDayPjpz5GM5krcI8dCWVd8re0a9bGjjN
+ioWGlu/eTr5El0ffwCNZ2WLmL9rewfHf/bMvYz3ioFZJ2OTxfazqYXNggQz6cMfa
+lbedDCdt5XLVX2TyerGvFram+9Uyvk3l0uM7rZnwAmdirG4Tv94QRaD3q4xTj/c0
+mv+AggtK0aRFb9o47z/BypLdk5mhbf3Mmr88C8XBzEnfdYyf4JpTlZrYLBmDCu5d
+9RLLsjXxhag8xqMtd1uLUM8XOTGzVWacw8iGY+CTtBKqyA+AE6/bDwZvEwVtsKtC
+QJ85ioEpy00NioqcF0WyMZH80uMsPycfpnl5uF7RkW8u
+-----END CERTIFICATE-----
+`
+
+	otherCert = `-----BEGIN CERTIFICATE-----
+MIIBqjCCAU+gAwIBAgIRAPnGGsBUMbZhmh5QdnYdBmUwCgYIKoZIzj0EAwIwGjEY
+MBYGA1UEAxMPaW50ZXJtZWRpYXRlLWNhMB4XDTIyMDIwOTEwMjUzMVoXDTIyMDIx
+MDEwMjUzMVowDjEMMAoGA1UEAxMDZm9vMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcD
+QgAEqnxdeInykx8JZsLi13rZLekoG2cosQ3F+2InVNy7hCQ7soMqdaJsGQ6LFtov
+ogUFtOOTRWrunblqNWGZsowHbKOBgTB/MA4GA1UdDwEB/wQEAwIHgDAdBgNVHSUE
+FjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwHQYDVR0OBBYEFLtundVbuKd73OWzo6SY
+by0Ajeb2MB8GA1UdIwQYMBaAFCLg80J/bZBbOd+Y8+V94l5xM2zEMA4GA1UdEQQH
+MAWCA2ZvbzAKBggqhkjOPQQDAgNJADBGAiEA4K4SbVNqrEtl7RfwBfJFMnWI+X8D
+zMPMc4Xqzp2qTxcCIQDsySgtiakypZfWakpB49zJph0kLwGK8xhWvGMUw1N1/w==
+-----END CERTIFICATE-----
+`
+
+	keyData = `-----BEGIN PRIVATE KEY-----
+MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC3o6/JdZEqNbqN
+RkopHhJtJG5c4qS5d0tQ/kZYpfD/v/izAYum4NzjaG15owr92/11W0pxPUliRLti
+3y6iScTs+ofm2D7p4UXj/Fnho/2xoWSOoWAodgvWY8jh8A0LQALZiV/9QsrJdXZd
+S47DYZLsQ3z9yFC/CdXkg1l7AQ3fIVGKdrQBr9kE1gEDqnKfRxXI8DEQKXr+CKPU
+wCAytegmy0SHp53zNAvY+kopHytzmJpXLoEhxq4eugHe52vXHdh/HJ9VjNp0xOH1
+waAgAGxHlltCW0PVd5AJ0SXROBS/a3V9sZCbCrJaYOOonQSEswveSv6PcG9AHvpN
+Pot2Xs6hAgMBAAECggEACTGPrmVNZDCWa1Y2hkJ0J7SoNcw+9O4M/jwMp4l/PD6P
+I98S78LYLCZhPLK17SmjUcnFO1AXKW1JeFS2D/fjfP256guvcqQNjLFoioxcOhVb
+ZGyd1Mi8JPqP5wfOj16gBeYDwTkjz9wqldcfiZaL9XoXetkZecbzR2JwC2FtIVuC
+0njTjMNYpaBKnoLb8OTR0EQz7lYEo2MkQiWryz8wseONnFmdfh18p+p10YgCbuCH
+qesrWfDLLxaxZelNtDhDngg9LoCLmarYy7BgShacmUEgJTZ/x3xFC75thK3ln0OY
++ktTgvVotYYaZi7qAjQiEsTvkTAPg5RMpQLd2UIWsQKBgQDCBp+1vURbwGzmTNUg
+HMipD6WDFdLc9DCacx6+ZqsEPTMWQbCpVZrDKiY0Rjt5F+xOCyMr00J5RDJXRC0G
++L7NcJdywOFutT7vB+cmETg7l/6PHweNYBnE66706eTL/KVYZMi4tEinarPWhHmL
+jasfdLANtpDjdWkRt299TkPRbQKBgQDyS8Rr7KZdv04Csqkf+ASmiJpT5R6Y72kc
+3XYpKETyB2FyPZkuh/zInMut9SkkSI9O/jA3zf956jj6sF1DHvp7T8KkIp5OAQeD
+J9AF65m2MnZfHFUeJ6ZQsggwMWqrD0ycIWP7YWtiBHH+D1wGkjYrssq+bvG/yNpA
+LtqdKq9lhQKBgQCZA2hIhy61vRckuEsLvCdzTGeW7UsR/XGnHEqOlaEhArKbRsrv
+gBdA+qiOaSTV5svw8E+YbE7sG6AnuhhYeyreEYEeeoZOLJmpIG5mUwYp2UBj1nC6
+SaOI7OVZOGu7g09SWokBQQxbG4cgEfFY4Sym7fs5lVTGTP3Dfwppo6NQMQKBgQCo
+J5NDP3Lafwk58BpV+H/pv8YzUUDh7M2rXbtCpxLqUdr8OOnVlEUISWFF8m5CIyVq
+MhjuscWLK9Wtjba7/YTjDaDM3sW05xv6lyfU5ATCoNTr/zLHgcb4HAZ4w+L+otiN
+RtMnxB2NYf5mzuwUF2cG/secUEzwyAlIH/xStSwTLQKBgQCRvqF+rqxnegoOgwVW
+qrWPv06wXD8dW2FlPpY5GXqA0l6erSK3YsQQToRmbem9ibPD7bd5P4gNbWfxwK4C
+Wt+1Rcb8OrDhDJbYz85bXBnPecKp4EN0b9SHO0/dsCqn2w30emc+9T/4m1ZDkpBd
+BixHvI/EJ8YK3ta5WdJWKC6hnA==
+-----END PRIVATE KEY-----
+`
+)
+
+const (
+	filterPrivateKey = "private key"
+	filterCert       = "certificate"
+)
+
+func TestFilterPEM(t *testing.T) {
+	type args struct {
+		input   string
+		pemType string
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    string
+		wantErr bool
+	}{
+		{
+			name: "extract cert / cert first",
+			args: args{
+				input:   certData + keyData,
+				pemType: filterCert,
+			},
+			want: certData,
+		},
+		{
+			name: "extract cert / key first",
+			args: args{
+				input:   keyData + certData,
+				pemType: filterCert,
+			},
+			want: certData,
+		},
+		{
+			name: "extract multiple certs",
+			args: args{
+				input:   keyData + certData + keyData + otherCert,
+				pemType: filterCert,
+			},
+			want: certData + otherCert,
+		},
+		{
+			name: "extract key",
+			args: args{
+				input:   keyData + certData,
+				pemType: filterPrivateKey,
+			},
+			want: keyData,
+		},
+		{
+			name: "key with junk",
+			args: args{
+				input:   certData + keyData + "some ---junk---",
+				pemType: filterPrivateKey,
+			},
+			want: keyData,
+		},
+		{
+			name: "begin/end with junk",
+			args: args{
+				// pem.Decode trims junk from the beginning of the input
+				// so we are able to decode both cert & key
+				input:   "some junk" + certData + keyData + "some ---junk---",
+				pemType: filterPrivateKey,
+			},
+			want: keyData,
+		},
+		{
+			name: "interleaved junk",
+			args: args{
+				// can parse cert but not key due to junk
+				input:   certData + "some junk" + keyData,
+				pemType: filterPrivateKey,
+			},
+			wantErr: true,
+		},
+		{
+			name: "err when junk",
+			args: args{
+				input:   "---junk---",
+				pemType: filterPrivateKey,
+			},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, err := filterPEM(tt.args.pemType, tt.args.input)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("filterPEM() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if got != tt.want {
+				t.Errorf("filterPEM() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}

+ 109 - 0
pkg/template/v2/pkcs12.go

@@ -0,0 +1,109 @@
+/*
+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"
+	"crypto/x509"
+	"encoding/pem"
+	"fmt"
+
+	"golang.org/x/crypto/pkcs12"
+)
+
+func pkcs12keyPass(pass, input string) (string, error) {
+	blocks, err := pkcs12.ToPEM([]byte(input), pass)
+	if err != nil {
+		return "", fmt.Errorf(errDecodePKCS12WithPass, err)
+	}
+
+	var pemData []byte
+	for _, block := range blocks {
+		// remove bag attributes like localKeyID, friendlyName
+		block.Headers = nil
+		if block.Type == pemTypeCertificate {
+			continue
+		}
+		key, err := parsePrivateKey(block.Bytes)
+		if err != nil {
+			return "", err
+		}
+		// we use pkcs8 because it supports more key types (ecdsa, ed25519), not just RSA
+		block.Bytes, err = x509.MarshalPKCS8PrivateKey(key)
+		if err != nil {
+			return "", err
+		}
+		// report error if encode fails
+		var buf bytes.Buffer
+		if err := pem.Encode(&buf, block); err != nil {
+			return "", err
+		}
+		pemData = append(pemData, buf.Bytes()...)
+	}
+
+	return string(pemData), nil
+}
+
+func parsePrivateKey(block []byte) (interface{}, error) {
+	if k, err := x509.ParsePKCS1PrivateKey(block); err == nil {
+		return k, nil
+	}
+	if k, err := x509.ParsePKCS8PrivateKey(block); err == nil {
+		return k, nil
+	}
+	if k, err := x509.ParseECPrivateKey(block); err == nil {
+		return k, nil
+	}
+	return nil, fmt.Errorf(errParsePrivKey)
+}
+
+func pkcs12key(input string) (string, error) {
+	return pkcs12keyPass("", input)
+}
+
+func pkcs12certPass(pass, input string) (string, error) {
+	blocks, err := pkcs12.ToPEM([]byte(input), pass)
+	if err != nil {
+		return "", fmt.Errorf(errDecodeCertWithPass, err)
+	}
+
+	var pemData []byte
+	for _, block := range blocks {
+		if block.Type != pemTypeCertificate {
+			continue
+		}
+		// remove bag attributes like localKeyID, friendlyName
+		block.Headers = nil
+		// report error if encode fails
+		var buf bytes.Buffer
+		if err := pem.Encode(&buf, block); err != nil {
+			return "", err
+		}
+		pemData = append(pemData, buf.Bytes()...)
+	}
+
+	// try to order certificate chain. If it fails we return
+	// the unordered raw pem data.
+	// This fails if multiple leaf or disjunct certs are provided.
+	ordered, err := fetchCertChains(pemData)
+	if err != nil {
+		return string(pemData), nil
+	}
+
+	return string(ordered), nil
+}
+
+func pkcs12cert(input string) (string, error) {
+	return pkcs12certPass("", input)
+}

+ 95 - 0
pkg/template/v2/template.go

@@ -0,0 +1,95 @@
+/*
+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"
+	"fmt"
+	tpl "text/template"
+
+	"github.com/Masterminds/sprig/v3"
+	corev1 "k8s.io/api/core/v1"
+)
+
+var tplFuncs = tpl.FuncMap{
+	"pkcs12key":      pkcs12key,
+	"pkcs12keyPass":  pkcs12keyPass,
+	"pkcs12cert":     pkcs12cert,
+	"pkcs12certPass": pkcs12certPass,
+
+	"filterPEM": filterPEM,
+
+	"jwkPublicKeyPem":  jwkPublicKeyPem,
+	"jwkPrivateKeyPem": jwkPrivateKeyPem,
+}
+
+// So other templating calls can use the same extra functions.
+func FuncMap() tpl.FuncMap {
+	return tplFuncs
+}
+
+const (
+	errParse                = "unable to parse template at key %s: %s"
+	errExecute              = "unable to execute template at key %s: %s"
+	errDecodePKCS12WithPass = "unable to decode pkcs12 with password: %s"
+	errDecodeCertWithPass   = "unable to decode pkcs12 certificate with password: %s"
+	errParsePrivKey         = "unable to parse private key type"
+
+	pemTypeCertificate = "CERTIFICATE"
+)
+
+func init() {
+	sprigFuncs := sprig.TxtFuncMap()
+	delete(sprigFuncs, "env")
+	delete(sprigFuncs, "expandenv")
+
+	for k, v := range sprigFuncs {
+		tplFuncs[k] = v
+	}
+}
+
+// Execute renders the secret data as template. If an error occurs processing is stopped immediately.
+func Execute(tpl, data map[string][]byte, secret *corev1.Secret) error {
+	if tpl == nil {
+		return nil
+	}
+	for k, v := range tpl {
+		val, err := execute(k, string(v), data)
+		if err != nil {
+			return fmt.Errorf(errExecute, k, err)
+		}
+		secret.Data[k] = val
+	}
+	return nil
+}
+
+func execute(k, val string, data map[string][]byte) ([]byte, error) {
+	strValData := make(map[string]string, len(data))
+	for k := range data {
+		strValData[k] = string(data[k])
+	}
+
+	t, err := tpl.New(k).
+		Funcs(tplFuncs).
+		Parse(val)
+	if err != nil {
+		return nil, fmt.Errorf(errParse, k, err)
+	}
+	buf := bytes.NewBuffer(nil)
+	err = t.Execute(buf, strValData)
+	if err != nil {
+		return nil, fmt.Errorf(errExecute, k, err)
+	}
+	return buf.Bytes(), nil
+}

Plik diff jest za duży
+ 26 - 0
pkg/template/v2/template_test.go


Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików