소스 검색

Merge branch 'main' into feature/conversion-webhook
Fixed conflicts and implemented necessary changes for v1beta1

Gustavo Carvalho 4 년 전
부모
커밋
40ec693479
63개의 변경된 파일1742개의 추가작업 그리고 146개의 파일을 삭제
  1. 9 9
      .github/workflows/e2e-managed.yml
  2. 4 4
      .github/workflows/e2e.yml
  3. 1 0
      ADOPTERS.md
  4. 2 2
      Makefile
  5. 3 3
      README.md
  6. 13 0
      apis/externalsecrets/v1alpha1/externalsecret_types.go
  7. 32 1
      apis/externalsecrets/v1beta1/externalsecret_types.go
  8. 14 0
      config/crds/bases/external-secrets.io_externalsecrets.yaml
  9. 11 0
      deploy/crds/bundle.yaml
  10. 50 0
      docs/guides-templating-v1.md
  11. 130 28
      docs/guides-templating.md
  12. 25 0
      docs/snippets/jwk-template-v2-external-secret.yaml
  13. 4 0
      docs/snippets/multiline-template-v1-external-secret.yaml
  14. 32 0
      docs/snippets/multiline-template-v2-external-secret.yaml
  15. 0 0
      docs/snippets/pkcs12-template-v1-external-secret.yaml
  16. 19 0
      docs/snippets/pkcs12-template-v2-external-secret.yaml
  17. 0 0
      docs/snippets/template-v1-from-secret.yaml
  18. 41 0
      docs/snippets/template-v2-from-secret.yaml
  19. 36 0
      docs/spec.md
  20. 3 2
      e2e/framework/framework.go
  21. 1 0
      e2e/suite/import.go
  22. 89 0
      e2e/suite/template/provider.go
  23. 101 0
      e2e/suite/template/template.go
  24. 15 14
      go.mod
  25. 38 28
      go.sum
  26. 1 1
      hack/api-docs/Dockerfile
  27. 2 4
      hack/api-docs/Makefile
  28. 3 1
      hack/api-docs/mkdocs.yml
  29. 15 15
      hack/api-docs/requirements.txt
  30. 5 1
      pkg/controllers/externalsecret/externalsecret_controller_template.go
  31. 21 1
      pkg/controllers/externalsecret/externalsecret_controller_test.go
  32. 15 0
      pkg/provider/gcp/secretmanager/secretsmanager.go
  33. 5 0
      pkg/provider/gcp/secretmanager/secretsmanager_workload_identity.go
  34. 4 0
      pkg/provider/gcp/secretmanager/secretsmanager_workload_identity_test.go
  35. 54 28
      pkg/provider/vault/vault.go
  36. 22 0
      pkg/provider/vault/vault_test.go
  37. 2 2
      pkg/provider/webhook/webhook.go
  38. 36 0
      pkg/template/engine.go
  39. 0 2
      pkg/template/v1/template.go
  40. 0 0
      pkg/template/v1/template_test.go
  41. 80 0
      pkg/template/v2/_testdata/Makefile
  42. 31 0
      pkg/template/v2/_testdata/chain.pem
  43. 29 0
      pkg/template/v2/_testdata/disjunct-chain.pem
  44. 9 0
      pkg/template/v2/_testdata/disjunct-root-ca.crt
  45. 3 0
      pkg/template/v2/_testdata/disjunct-root-ca.key
  46. BIN
      pkg/template/v2/_testdata/foo-disjunct-nopass.pfx
  47. BIN
      pkg/template/v2/_testdata/foo-multibag-nopass.pfx
  48. BIN
      pkg/template/v2/_testdata/foo-nopass.pfx
  49. BIN
      pkg/template/v2/_testdata/foo-withpass-1234.pfx
  50. 11 0
      pkg/template/v2/_testdata/foo.crt
  51. 5 0
      pkg/template/v2/_testdata/foo.key
  52. 11 0
      pkg/template/v2/_testdata/intermediate-ca.crt
  53. 5 0
      pkg/template/v2/_testdata/intermediate-ca.key
  54. 20 0
      pkg/template/v2/_testdata/intermediate-chain.pem
  55. 9 0
      pkg/template/v2/_testdata/root-ca.crt
  56. 3 0
      pkg/template/v2/_testdata/root-ca.key
  57. 55 0
      pkg/template/v2/jwk.go
  58. 62 0
      pkg/template/v2/pem.go
  59. 141 0
      pkg/template/v2/pem_chain.go
  60. 180 0
      pkg/template/v2/pem_test.go
  61. 109 0
      pkg/template/v2/pkcs12.go
  62. 95 0
      pkg/template/v2/template.go
  63. 26 0
      pkg/template/v2/template_test.go

+ 9 - 9
.github/workflows/e2e-managed.yml

@@ -49,7 +49,7 @@ jobs:
     steps:
 
     # create new status check for this specific provider
-    - uses: actions/github-script@v1
+    - uses: actions/github-script@v6
       if: ${{ always() }}
       env:
         number: ${{ github.event.client_payload.pull_request.number }}
@@ -58,13 +58,13 @@ jobs:
       with:
         github-token: ${{ secrets.GITHUB_TOKEN }}
         script: |
-          const { data: pull } = await github.pulls.get({
+          const { data: pull } = await github.rest.pulls.get({
             ...context.repo,
             pull_number: process.env.number
           });
           const ref = pull.head.sha;
           console.log("\n\nPR sha: " + ref)
-          const { data: checks } = await github.checks.listForRef({
+          const { data: checks } = await github.rest.checks.listForRef({
             ...context.repo,
             ref
           });
@@ -74,14 +74,14 @@ jobs:
           console.log("\n\nPR Filtered CHECK: " + check)
           console.log(check)
           if(check && check.length > 0){
-            const { data: result } = await github.checks.update({
+            const { data: result } = await github.rest.checks.update({
               ...context.repo,
               check_run_id: check[0].id,
               status: 'in_progress',
             });
             return result;
           }
-          const { data: result } = await github.checks.create({
+          const { data: result } = await github.rest.checks.create({
             ...context.repo,
             name: job_name,
             head_sha: pull.head.sha,
@@ -201,7 +201,7 @@ jobs:
         make tf.destroy.${PROVIDER}
 
     # set status=completed
-    - uses: actions/github-script@v1
+    - uses: actions/github-script@v6
       if: ${{ always() }}
       env:
         number: ${{ github.event.client_payload.pull_request.number }}
@@ -212,13 +212,13 @@ jobs:
       with:
         github-token: ${{ secrets.GITHUB_TOKEN }}
         script: |
-          const { data: pull } = await github.pulls.get({
+          const { data: pull } = await github.rest.pulls.get({
             ...context.repo,
             pull_number: process.env.number
           });
           const ref = pull.head.sha;
           console.log("\n\nPR sha: " + ref)
-          const { data: checks } = await github.checks.listForRef({
+          const { data: checks } = await github.rest.checks.listForRef({
             ...context.repo,
             ref
           });
@@ -227,7 +227,7 @@ jobs:
           const check = checks.check_runs.filter(c => c.name === job_name);
           console.log("\n\nPR Filtered CHECK: " + check)
           console.log(check)
-          const { data: result } = await github.checks.update({
+          const { data: result } = await github.rest.checks.update({
             ...context.repo,
             check_run_id: check[0].id,
             status: 'completed',

+ 4 - 4
.github/workflows/e2e.yml

@@ -156,7 +156,7 @@ jobs:
         make test.e2e
 
     # Update check run called "integration-fork"
-    - uses: actions/github-script@v1
+    - uses: actions/github-script@v6
       id: update-check-run
       if: ${{ always() }}
       env:
@@ -167,13 +167,13 @@ jobs:
       with:
         github-token: ${{ secrets.GITHUB_TOKEN }}
         script: |
-          const { data: pull } = await github.pulls.get({
+          const { data: pull } = await github.rest.pulls.get({
             ...context.repo,
             pull_number: process.env.number
           });
           const ref = pull.head.sha;
           console.log("\n\nPR sha: " + ref)
-          const { data: checks } = await github.checks.listForRef({
+          const { data: checks } = await github.rest.checks.listForRef({
             ...context.repo,
             ref
           });
@@ -181,7 +181,7 @@ jobs:
           const check = checks.check_runs.filter(c => c.name === process.env.job);
           console.log("\n\nPR Filtered CHECK: " + check)
           console.log(check)
-          const { data: result } = await github.checks.update({
+          const { data: result } = await github.rest.checks.update({
             ...context.repo,
             check_run_id: check[0].id,
             status: 'completed',

+ 1 - 0
ADOPTERS.md

@@ -2,6 +2,7 @@
 
 <!-- Add yourself here if you are using ESO in your company or your project! -->
 
+- [Polarpoint](https://www.polarpoint.io/)
 - [Pento](https://www.pento.io/)
 - [Mixpanel](https://mixpanel.com)
 - [K8S Website Infra](https://k8s.io/)

+ 2 - 2
Makefile

@@ -196,8 +196,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
 
 # ====================================================================================

+ 3 - 3
README.md

@@ -30,8 +30,8 @@ Multiple people and organizations are joining efforts to create a single Externa
 
 | Provider                                                                 | Stability |                                        Contact |
 | ------------------------------------------------------------------------ | :-------: | ---------------------------------------------: |
-| [AWS SM](https://external-secrets.io/provider-aws-secrets-manager/)      |   beta   | [ESO Org](https://github.com/external-secrets) |
-| [AWS PS](https://external-secrets.io/provider-aws-parameter-store/)      |   beta   | [ESO Org](https://github.com/external-secrets) |
+| [AWS SM](https://external-secrets.io/provider-aws-secrets-manager/)      |   stable   | [ESO Org](https://github.com/external-secrets) |
+| [AWS PS](https://external-secrets.io/provider-aws-parameter-store/)      |   stable   | [ESO Org](https://github.com/external-secrets) |
 | [Hashicorp Vault](https://external-secrets.io/provider-hashicorp-vault/) |   stable   | [ESO Org](https://github.com/external-secrets) |
 | [GCP SM](https://external-secrets.io/provider-google-secrets-manager/)   |   stable | [ESO Org](https://github.com/external-secrets) |
 
@@ -39,7 +39,7 @@ Multiple people and organizations are joining efforts to create a single Externa
 
 | Provider                                                            | Stability |                  Contact                   |
 | ------------------------------------------------------------------- | :-------: | :----------------------------------------: |
-| [Azure KV](https://external-secrets.io/provider-azure-key-vault/)   |   alpha   | [@ahmedmus-1A](https://github.com/ahmedmus-1A) [@asnowfix](https://github.com/asnowfix) [@ncourbet-1A](https://github.com/ncourbet-1A) [@1A-mj](https://github.com/1A-mj) |
+| [Azure KV](https://external-secrets.io/provider-azure-key-vault/)   |   beta   | [@ahmedmus-1A](https://github.com/ahmedmus-1A) [@asnowfix](https://github.com/asnowfix) [@ncourbet-1A](https://github.com/ncourbet-1A) [@1A-mj](https://github.com/1A-mj) |
 | [IBM SM](https://external-secrets.io/provider-ibm-secrets-manager/) |   alpha   |   [@knelasevero](https://github.com/knelasevero) [@sebagomez](https://github.com/sebagomez) [@ricardoptcosta](https://github.com/ricardoptcosta)  |
 | [Yandex Lockbox](https://external-secrets.io/provider-yandex-lockbox/) |   alpha   |   [@AndreyZamyslov](https://github.com/AndreyZamyslov) [@knelasevero](https://github.com/knelasevero)          |
 | [Gitlab Project Variables](https://external-secrets.io/provider-gitlab-project-variables/) |   alpha   |   [@Jabray5](https://github.com/Jabray5)          |

+ 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 {

+ 32 - 1
apis/externalsecrets/v1beta1/externalsecret_types.go

@@ -44,6 +44,20 @@ const (
 	None ExternalSecretCreationPolicy = "None"
 )
 
+// ExternalSecretDeletionPolicy defines rules on how to delete the resulting Secret.
+type ExternalSecretDeletionPolicy string
+
+const (
+	// Owner creates the Secret and sets .metadata.ownerReferences to the ExternalSecret resource.
+	DeletionOwner ExternalSecretDeletionPolicy = "Owner"
+
+	// Merge does not create the Secret, but merges the data fields to the Secret.
+	DeletionMerge ExternalSecretDeletionPolicy = "Merge"
+
+	// None does not create a Secret (future use with injector).
+	DeletionNone ExternalSecretDeletionPolicy = "None"
+)
+
 // ExternalSecretTemplateMetadata defines metadata fields for the Secret blueprint.
 type ExternalSecretTemplateMetadata struct {
 	// +optional
@@ -59,6 +73,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="v2"
+
+	EngineVersion TemplateEngineVersion `json:"engineVersion,omitempty"`
 	// +optional
 	Metadata ExternalSecretTemplateMetadata `json:"metadata,omitempty"`
 
@@ -69,6 +89,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 {
@@ -99,7 +126,11 @@ type ExternalSecretTarget struct {
 	// +optional
 	// +kubebuilder:default="Owner"
 	CreationPolicy ExternalSecretCreationPolicy `json:"creationPolicy,omitempty"`
-
+	// DeletionPolicy defines rules on how to delete the resulting Secret
+	// Defaults to 'None'
+	// +optional
+	// +kubebuilder:default="None"
+	DeletionPolicy ExternalSecretDeletionPolicy `json:"deletionPolicy,omitempty"`
 	// Template defines a blueprint for the created Secret resource.
 	// +optional
 	Template *ExternalSecretTemplate `json:"template,omitempty"`

+ 14 - 0
config/crds/bases/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.
@@ -381,6 +387,11 @@ spec:
                     description: CreationPolicy defines rules on how to create the
                       resulting Secret Defaults to 'Owner'
                     type: string
+                  deletionPolicy:
+                    default: None
+                    description: DeletionPolicy defines rules on how to delete the
+                      resulting Secret Defaults to 'None'
+                    type: string
                   immutable:
                     description: Immutable defines if the final secret will be immutable
                     type: boolean
@@ -397,6 +408,9 @@ spec:
                         additionalProperties:
                           type: string
                         type: object
+                      engineVersion:
+                        default: v2
+                        type: string
                       metadata:
                         description: ExternalSecretTemplateMetadata defines metadata
                           fields for the Secret blueprint.

+ 11 - 0
deploy/crds/bundle.yaml

@@ -1859,6 +1859,10 @@ 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.
                           properties:
@@ -2068,6 +2072,10 @@ spec:
                       default: Owner
                       description: CreationPolicy defines rules on how to create the resulting Secret Defaults to 'Owner'
                       type: string
+                    deletionPolicy:
+                      default: None
+                      description: DeletionPolicy defines rules on how to delete the resulting Secret Defaults to 'None'
+                      type: string
                     immutable:
                       description: Immutable defines if the final secret will be immutable
                       type: boolean
@@ -2081,6 +2089,9 @@ spec:
                           additionalProperties:
                             type: string
                           type: object
+                        engineVersion:
+                          default: v2
+                          type: string
                         metadata:
                           description: ExternalSecretTemplateMetadata defines metadata fields for the Secret blueprint.
                           properties:

+ 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",
+			},
+		}
+	}
+}

+ 15 - 14
go.mod

@@ -35,13 +35,12 @@ replace (
 require (
 	cloud.google.com/go v0.100.2 // indirect
 	cloud.google.com/go/secretmanager v1.0.0
-	github.com/Azure/azure-sdk-for-go v61.4.0+incompatible
-	github.com/Azure/go-autorest/autorest/azure/auth v0.5.7
+	github.com/Azure/azure-sdk-for-go v61.5.0+incompatible
+	github.com/Azure/go-autorest/autorest/azure/auth v0.5.11
 	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
@@ -57,7 +56,7 @@ require (
 	github.com/hashicorp/vault/api v1.3.1
 	github.com/huandu/xstrings v1.3.2 // indirect
 	github.com/lestrrat-go/jwx v1.2.1
-	github.com/onsi/ginkgo/v2 v2.1.1
+	github.com/onsi/ginkgo/v2 v2.1.2
 	github.com/onsi/gomega v1.18.1
 	github.com/oracle/oci-go-sdk/v56 v56.1.0
 	github.com/prometheus/client_golang v1.12.1
@@ -68,11 +67,11 @@ require (
 	github.com/yandex-cloud/go-genproto v0.0.0-20210809082946-a97da516c588
 	github.com/yandex-cloud/go-sdk v0.0.0-20210809100642-c13c40a429fa
 	github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a
-	go.uber.org/zap v1.20.0
+	go.uber.org/zap v1.21.0
 	golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce
 	golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8
-	google.golang.org/api v0.64.0
-	google.golang.org/genproto v0.0.0-20220118154757-00ab72f36ad5
+	google.golang.org/api v0.68.0
+	google.golang.org/genproto v0.0.0-20220204002441-d6cc3cc0770e
 	google.golang.org/grpc v1.44.0
 	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
 	grpc.go4.org v0.0.0-20170609214715-11d0a25b4919
@@ -92,16 +91,17 @@ require (
 )
 
 require (
-	cloud.google.com/go/compute v0.1.0 // indirect
+	cloud.google.com/go/compute v1.2.0 // indirect
 	github.com/Azure/go-autorest v14.2.0+incompatible // indirect
-	github.com/Azure/go-autorest/autorest v0.11.18 // indirect
-	github.com/Azure/go-autorest/autorest/adal v0.9.13 // indirect
-	github.com/Azure/go-autorest/autorest/azure/cli v0.4.2 // indirect
+	github.com/Azure/go-autorest/autorest v0.11.24 // indirect
+	github.com/Azure/go-autorest/autorest/adal v0.9.18 // indirect
+	github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 // indirect
 	github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
 	github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
 	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
@@ -116,7 +116,6 @@ require (
 	github.com/dimchansky/utfbom v1.1.1 // indirect
 	github.com/evanphx/json-patch v4.12.0+incompatible // indirect
 	github.com/fatih/color v1.13.0 // indirect
-	github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect
 	github.com/fsnotify/fsnotify v1.5.1 // indirect
 	github.com/ghodss/yaml v1.0.0 // indirect
 	github.com/go-logr/zapr v1.2.0 // indirect
@@ -184,7 +183,9 @@ require (
 	github.com/prometheus/procfs v0.7.3 // indirect
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
 	github.com/ryanuber/go-glob v1.0.0 // indirect
+	github.com/shopspring/decimal v1.2.0 // indirect
 	github.com/sony/gobreaker v0.4.2-0.20210216022020-dd874f9dd33b // indirect
+	github.com/spf13/cast v1.4.1 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
 	github.com/stretchr/objx v0.2.0 // indirect
 	github.com/tidwall/match v1.1.1 // indirect
@@ -195,7 +196,7 @@ require (
 	go.uber.org/multierr v1.6.0 // indirect
 	golang.org/x/mod v0.5.0 // indirect
 	golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d // indirect
-	golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect
+	golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a // indirect
 	golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect
 	golang.org/x/text v0.3.7 // indirect
 	golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect

+ 38 - 28
go.sum

@@ -36,8 +36,9 @@ cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvf
 cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
 cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
 cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
-cloud.google.com/go/compute v0.1.0 h1:rSUBvAyVwNJ5uQCKNJFMwPtTvJkfN38b6Pvb9zZoqJ8=
 cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow=
+cloud.google.com/go/compute v1.2.0 h1:EKki8sSdvDU0OO9mAXGwPXOTOgPz2l08R0/IutDH11I=
+cloud.google.com/go/compute v1.2.0/go.mod h1:xlogom/6gr8RJGBe7nT2eGsQYAFUbbv8dbC29qE3Xmw=
 cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
 cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
 cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
@@ -56,23 +57,22 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
 cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
 cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
-github.com/Azure/azure-sdk-for-go v61.4.0+incompatible h1:BF2Pm3aQWIa6q9KmxyF1JYKYXtVw67vtvu2Wd54NGuY=
-github.com/Azure/azure-sdk-for-go v61.4.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
+github.com/Azure/azure-sdk-for-go v61.5.0+incompatible h1:OSHSFeNm7D1InGsQrFjyN9hpxD5Ec60PdsWWudCpah4=
+github.com/Azure/azure-sdk-for-go v61.5.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
 github.com/Azure/go-ansiterm v0.0.0-20210608223527-2377c96fe795/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
 github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
 github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
 github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
-github.com/Azure/go-autorest/autorest v0.11.17/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw=
-github.com/Azure/go-autorest/autorest v0.11.18 h1:90Y4srNYrwOtAgVo3ndrQkTYn6kf1Eg/AjTFJ8Is2aM=
 github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA=
-github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A=
-github.com/Azure/go-autorest/autorest/adal v0.9.11/go.mod h1:nBKAnTomx8gDtl+3ZCJv2v0KACFHWTB2drffI1B68Pk=
-github.com/Azure/go-autorest/autorest/adal v0.9.13 h1:Mp5hbtOePIzM8pJVRa3YLrWWmZtoxRXqUEzCfJt3+/Q=
+github.com/Azure/go-autorest/autorest v0.11.24 h1:1fIGgHKqVm54KIPT+q8Zmd1QlVsmHqeUGso5qm2BqqE=
+github.com/Azure/go-autorest/autorest v0.11.24/go.mod h1:G6kyRlFnTuSbEYkQGawPfsCswgme4iYf6rfSKUDzbCc=
 github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M=
-github.com/Azure/go-autorest/autorest/azure/auth v0.5.7 h1:8DQB8yl7aLQuP+nuR5e2RO6454OvFlSTXXaNHshc16s=
-github.com/Azure/go-autorest/autorest/azure/auth v0.5.7/go.mod h1:AkzUsqkrdmNhfP2i54HqINVQopw0CLDnvHpJ88Zz1eI=
-github.com/Azure/go-autorest/autorest/azure/cli v0.4.2 h1:dMOmEJfkLKW/7JsokJqkyoYSgmR08hi9KrhjZb+JALY=
-github.com/Azure/go-autorest/autorest/azure/cli v0.4.2/go.mod h1:7qkJkT+j6b+hIpzMOwPChJhTqS8VbsqqgULzMNRugoM=
+github.com/Azure/go-autorest/autorest/adal v0.9.18 h1:kLnPsRjzZZUF3K5REu/Kc+qMQrvuza2bwSnNdhmzLfQ=
+github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ=
+github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 h1:P6bYXFoao05z5uhOQzbC3Qd8JqF3jUoocoTeIxkp2cA=
+github.com/Azure/go-autorest/autorest/azure/auth v0.5.11/go.mod h1:84w/uV8E37feW2NCJ08uT9VBfjfUHpgLVnG2InYD6cg=
+github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 h1:0W/yGmFdTIT77fvdlGZ0LMISoLHFJ7Tx4U0yeB+uFs4=
+github.com/Azure/go-autorest/autorest/azure/cli v0.4.5/go.mod h1:ADQAXrkgm7acgWVUNamOgh8YNrv4p27l3Wc55oVfpzg=
 github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw=
 github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=
 github.com/Azure/go-autorest/autorest/mocks v0.4.1 h1:K0laFcLE6VLTOwNgSxaGbUcLPuGXlNkbVvq4cW4nIHk=
@@ -81,7 +81,6 @@ github.com/Azure/go-autorest/autorest/to v0.4.0 h1:oXVqrxakqqV1UZdSazDOPOLvOIz+X
 github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE=
 github.com/Azure/go-autorest/autorest/validation v0.3.1 h1:AgyqjAd94fwNAoTjl/WQXg4VvFeRFpO+UhNyRXqF1ac=
 github.com/Azure/go-autorest/autorest/validation v0.3.1/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E=
-github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
 github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg=
 github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
 github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=
@@ -96,10 +95,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=
@@ -215,7 +214,6 @@ github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0/go.mod h1:J70FGZSbzsjecRTiTzER+
 github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
 github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
 github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
-github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
 github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U=
 github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE=
 github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
@@ -249,7 +247,6 @@ github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
 github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
 github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
-github.com/form3tech-oss/jwt-go v3.2.3+incompatible h1:7ZaBxOI7TMoYBfyA3cQHErNNyAWIKUMIwqxEtgHOs5c=
 github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
 github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y=
 github.com/frankban/quicktest v1.13.0 h1:yNZif1OkDfNoDfb9zZa9aXIpejNR4F23Wely0c+Qdqk=
@@ -340,6 +337,7 @@ github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXP
 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
 github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
+github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
 github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU=
 github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@@ -537,12 +535,14 @@ 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/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
 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=
@@ -706,8 +706,8 @@ github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vv
 github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
 github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
 github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
-github.com/onsi/ginkgo/v2 v2.1.1 h1:LCnPB85AvFNr91s0B2aDzEiiIg6MUwLYbryC1NSlWi8=
-github.com/onsi/ginkgo/v2 v2.1.1/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
+github.com/onsi/ginkgo/v2 v2.1.2 h1:QUvZA5LiZ5EMDS0dVTQbjOvYLFs3wzcztqFU/mfR70c=
+github.com/onsi/ginkgo/v2 v2.1.2/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
 github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
 github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
@@ -789,6 +789,8 @@ 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/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig=
 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/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
@@ -810,6 +812,7 @@ github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY52
 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/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA=
 github.com/spf13/cast v1.4.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=
@@ -922,8 +925,8 @@ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
 go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
 go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
 go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
-go.uber.org/zap v1.20.0 h1:N4oPlghZwYG55MlU6LXk/Zp00FVNE9X9wrYO8CEs4lc=
-go.uber.org/zap v1.20.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
+go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=
+go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@@ -935,13 +938,15 @@ golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
 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=
-golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
 golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
 golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce h1:Roh6XWxHFKrPgC/EQhVubSAGQ6Ozk6IdxHSzt1mR0EI=
 golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -1159,8 +1164,9 @@ golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0=
 golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a h1:ppl5mZgokTT8uPkmYOyEUmPTr3ypaKkg5eFOGrAmxxE=
+golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE=
@@ -1297,8 +1303,10 @@ google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUb
 google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=
 google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw=
 google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo=
-google.golang.org/api v0.64.0 h1:l3pi8ncrQgB9+ncFw3A716L8lWujnXniBYbxWqqy6tE=
 google.golang.org/api v0.64.0/go.mod h1:931CdxA8Rm4t6zqTFGSsgwbAEZ2+GMYurbndwSimebM=
+google.golang.org/api v0.66.0/go.mod h1:I1dmXYpX7HGwz/ejRxwQp2qj5bFAz93HiCU1C1oYd9M=
+google.golang.org/api v0.68.0 h1:9eJiHhwJKIYX6sX2fUZxQLi7pDRA/MYu8c12q6WbJik=
+google.golang.org/api v0.68.0/go.mod h1:sOM8pTpwgflXRhz+oC8H2Dr+UcbMqkPPWNJo88Q7TH8=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -1381,8 +1389,10 @@ google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ6
 google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
 google.golang.org/genproto v0.0.0-20211223182754-3ac035c7e7cb/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
 google.golang.org/genproto v0.0.0-20220111164026-67b88f271998/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20220118154757-00ab72f36ad5 h1:zzNejm+EgrbLfDZ6lu9Uud2IVvHySPl8vQzf04laR5Q=
-google.golang.org/genproto v0.0.0-20220118154757-00ab72f36ad5/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20220114231437-d2e6a121cae0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20220201184016-50beb8ab5c44/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20220204002441-d6cc3cc0770e h1:hXl9hnyOkeznztYpYxVPAVZfPzcbO6Q0C+nLXodza8k=
+google.golang.org/genproto v0.0.0-20220204002441-d6cc3cc0770e/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
 google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=

+ 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 *esv1beta1.ExternalSe
 	}
 	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 - 1
pkg/controllers/externalsecret/externalsecret_controller_test.go

@@ -432,7 +432,8 @@ var _ = Describe("ExternalSecret controller", func() {
 					"hihi": "ga",
 				},
 			},
-			Type: v1.SecretTypeOpaque,
+			Type:          v1.SecretTypeOpaque,
+			EngineVersion: esv1beta1.TemplateEngineV1,
 			Data: map[string]string{
 				targetProp:   targetPropObj,
 				tplStaticKey: tplStaticVal,
@@ -452,6 +453,24 @@ 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 = &esv1beta1.ExternalSecretTemplate{
+			Type: v1.SecretTypeOpaque,
+			// it should default to v2 for beta11
+			// EngineVersion: esv1beta1.TemplateEngineV2,
+			Data: map[string]string{
+				targetProp: "{{ .targetProperty | upper }} was templated",
+			},
+		}
+		fakeProvider.WithGetSecret([]byte(secretVal), nil)
+		tc.checkSecret = func(es *esv1beta1.ExternalSecret, secret *v1.Secret) {
+			// check values
+			Expect(string(secret.Data[targetProp])).To(Equal(expectedSecretVal))
+		}
+	}
+
 	// secret should be synced with correct value precedence:
 	// * template
 	// * templateFrom
@@ -1079,6 +1098,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),

+ 15 - 0
pkg/provider/gcp/secretmanager/secretsmanager.go

@@ -63,6 +63,7 @@ type GoogleSecretManagerClient interface {
 type ProviderGCP struct {
 	projectID           string
 	SecretManagerClient GoogleSecretManagerClient
+	gClient             *gClient
 }
 
 type gClient struct {
@@ -86,6 +87,10 @@ func (c *gClient) getTokenSource(ctx context.Context, store esv1beta1.GenericSto
 	return google.DefaultTokenSource(ctx, CloudPlatformRole)
 }
 
+func (c *gClient) Close() error {
+	return c.workloadIdentity.Close()
+}
+
 func serviceAccountTokenSource(ctx context.Context, store esv1beta1.GenericStore, kube kclient.Client, namespace string) (oauth2.TokenSource, error) {
 	spec := store.GetSpec()
 	if spec == nil || spec.Provider.GCPSM == nil {
@@ -146,6 +151,13 @@ func (sm *ProviderGCP) NewClient(ctx context.Context, store esv1beta1.GenericSto
 		storeKind:        store.GetObjectKind().GroupVersionKind().Kind,
 		workloadIdentity: wi,
 	}
+	sm.gClient = &cliStore
+	defer func() {
+		// closes IAMClient to prevent gRPC connection leak in case of an error.
+		if sm.SecretManagerClient == nil {
+			_ = sm.gClient.Close()
+		}
+	}()
 
 	sm.projectID = cliStore.store.ProjectID
 
@@ -245,6 +257,9 @@ func (sm *ProviderGCP) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalS
 
 func (sm *ProviderGCP) Close(ctx context.Context) error {
 	err := sm.SecretManagerClient.Close()
+	if sm.gClient != nil {
+		err = sm.gClient.Close()
+	}
 	if err != nil {
 		return fmt.Errorf(errClientClose, err)
 	}

+ 5 - 0
pkg/provider/gcp/secretmanager/secretsmanager_workload_identity.go

@@ -62,6 +62,7 @@ type workloadIdentity struct {
 // interface to GCP IAM API.
 type IamClient interface {
 	GenerateAccessToken(ctx context.Context, req *credentialspb.GenerateAccessTokenRequest, opts ...gax.CallOption) (*credentialspb.GenerateAccessTokenResponse, error)
+	Close() error
 }
 
 // interface to securetoken/identitybindingtoken API.
@@ -154,6 +155,10 @@ func (w *workloadIdentity) TokenSource(ctx context.Context, store esv1beta1.Gene
 	}), nil
 }
 
+func (w *workloadIdentity) Close() error {
+	return w.iamClient.Close()
+}
+
 func newIAMClient(ctx context.Context) (IamClient, error) {
 	iamOpts := []option.ClientOption{
 		option.WithUserAgent("external-secrets-operator"),

+ 4 - 0
pkg/provider/gcp/secretmanager/secretsmanager_workload_identity_test.go

@@ -357,6 +357,10 @@ func (f *fakeIAMClient) GenerateAccessToken(ctx context.Context, req *credential
 	return f.generateAccessTokenFunc(ctx, req, opts...)
 }
 
+func (f *fakeIAMClient) Close() error {
+	return nil
+}
+
 // fake SA Token Generator.
 type fakeSATokenGen struct {
 	GenerateFunc func(context.Context, string, string, string) (*authv1.TokenRequest, error)

+ 54 - 28
pkg/provider/vault/vault.go

@@ -58,6 +58,7 @@ const (
 	errDataField          = "failed to find data field"
 	errJSONUnmarshall     = "failed to unmarshall JSON"
 	errSecretFormat       = "secret data not in expected format"
+	errUnexpectedKey      = "unexpected key in data: %s"
 	errVaultToken         = "cannot parse Vault authentication token: %w"
 	errVaultReqParams     = "cannot set Vault request parameters: %w"
 	errVaultRequest       = "error from Vault request: %w"
@@ -163,24 +164,43 @@ func (v *client) GetAllSecrets(ctx context.Context, ref esv1beta1.ExternalSecret
 	return nil, fmt.Errorf("GetAllSecrets not implemented")
 }
 
+// GetSecret supports two types:
+// 1. get the full secret as json-encoded value
+//    by leaving the ref.Property empty.
+// 2. get a key from the secret.
+//    Nested values are supported by specifying a gjson expression
 func (v *client) GetSecret(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
 	data, err := v.readSecret(ctx, ref.Key, ref.Version)
 	if err != nil {
 		return nil, err
 	}
-
-	// return raw json if no property is defined
+	jsonStr, err := json.Marshal(data)
+	if err != nil {
+		return nil, err
+	}
+	// (1): return raw json if no property is defined
 	if ref.Property == "" {
-		return data, nil
+		return jsonStr, nil
 	}
 
-	val := gjson.Get(string(data), ref.Property)
+	// For backwards compatibility we want the
+	// actual keys to take precedence over gjson syntax
+	// (2): extract key from secret with property
+	if _, ok := data[ref.Property]; ok {
+		return getTypedKey(data, ref.Property)
+	}
+
+	// (3): extract key from secret using gjson
+	val := gjson.Get(string(jsonStr), ref.Property)
 	if !val.Exists() {
 		return nil, fmt.Errorf(errSecretKeyFmt, ref.Property)
 	}
 	return []byte(val.String()), nil
 }
 
+// GetSecretMap supports two modes of operation:
+// 1. get the full secret from the vault data payload (by leaving .property empty).
+// 2. extract key/value pairs from a (nested) object.
 func (v *client) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
 	data, err := v.GetSecret(ctx, ref)
 	if err != nil {
@@ -193,33 +213,40 @@ func (v *client) GetSecretMap(ctx context.Context, ref esv1beta1.ExternalSecretD
 		return nil, err
 	}
 	byteMap := make(map[string][]byte, len(secretData))
-	for k, v := range secretData {
-		switch t := v.(type) {
-		case string:
-			byteMap[k] = []byte(t)
-		case map[string]interface{}:
-			jsonData, err := json.Marshal(t)
-			if err != nil {
-				return nil, err
-			}
-			byteMap[k] = jsonData
-		case []byte:
-			byteMap[k] = t
-		// also covers int and float32 due to json.Marshal
-		case float64:
-			byteMap[k] = []byte(strconv.FormatFloat(t, 'f', -1, 64))
-		case bool:
-			byteMap[k] = []byte(strconv.FormatBool(t))
-		case nil:
-			byteMap[k] = []byte(nil)
-		default:
-			return nil, errors.New(errSecretFormat)
+	for k := range secretData {
+		byteMap[k], err = getTypedKey(secretData, k)
+		if err != nil {
+			return nil, err
 		}
 	}
 
 	return byteMap, nil
 }
 
+func getTypedKey(data map[string]interface{}, key string) ([]byte, error) {
+	v, ok := data[key]
+	if !ok {
+		return nil, fmt.Errorf(errUnexpectedKey, key)
+	}
+	switch t := v.(type) {
+	case string:
+		return []byte(t), nil
+	case map[string]interface{}:
+		return json.Marshal(t)
+	case []byte:
+		return t, nil
+	// also covers int and float32 due to json.Marshal
+	case float64:
+		return []byte(strconv.FormatFloat(t, 'f', -1, 64)), nil
+	case bool:
+		return []byte(strconv.FormatBool(t)), nil
+	case nil:
+		return []byte(nil), nil
+	default:
+		return nil, errors.New(errSecretFormat)
+	}
+}
+
 func (v *client) Close(ctx context.Context) error {
 	// Revoke the token if we have one set and it wasn't sourced from a TokenSecretRef
 	if v.client.Token() != "" && v.store.Auth.TokenSecretRef == nil {
@@ -268,7 +295,7 @@ func (v *client) buildPath(path string) string {
 	return returnPath
 }
 
-func (v *client) readSecret(ctx context.Context, path, version string) ([]byte, error) {
+func (v *client) readSecret(ctx context.Context, path, version string) (map[string]interface{}, error) {
 	dataPath := v.buildPath(path)
 
 	// path formated according to vault docs for v1 and v2 API
@@ -304,8 +331,7 @@ func (v *client) readSecret(ctx context.Context, path, version string) ([]byte,
 		}
 	}
 
-	// return json string
-	return json.Marshal(secretData)
+	return secretData, nil
 }
 
 func (v *client) newConfig() (*vault.Config, error) {

+ 22 - 0
pkg/provider/vault/vault_test.go

@@ -565,8 +565,10 @@ func TestGetSecret(t *testing.T) {
 	secretWithNestedVal := map[string]interface{}{
 		"access_key":    "access_key",
 		"access_secret": "access_secret",
+		"nested.bar":    "something different",
 		"nested": map[string]string{
 			"foo": "oke",
+			"bar": "also ok?",
 		},
 	}
 
@@ -662,6 +664,26 @@ func TestGetSecret(t *testing.T) {
 				val: []byte("oke"),
 			},
 		},
+		"ReadSecretWithNestedValueFromData": {
+			reason: "Should return a nested property",
+			args: args{
+				store: makeValidSecretStoreWithVersion(esv1beta1.VaultKVStoreV1).Spec.Provider.Vault,
+				data: esv1beta1.ExternalSecretDataRemoteRef{
+					//
+					Property: "nested.bar",
+				},
+				vClient: &fake.VaultClient{
+					MockNewRequest: fake.NewMockNewRequestFn(&vault.Request{}),
+					MockRawRequestWithContext: fake.NewMockRawRequestWithContextFn(
+						newVaultResponseWithData(secretWithNestedVal), nil,
+					),
+				},
+			},
+			want: want{
+				err: nil,
+				val: []byte("something different"),
+			},
+		},
 		"NonexistentProperty": {
 			reason: "Should return error property does not exist.",
 			args: args{

+ 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/v1beta1"
+	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
+}

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 26 - 0
pkg/template/v2/template_test.go


이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.