Browse Source

feat: implement template engine v2

Signed-off-by: Moritz Johner <beller.moritz@googlemail.com>
Moritz Johner 4 years ago
parent
commit
54e68399ec

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

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

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

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

+ 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{
+			"my-data": "executed: {{ .singlefoo | toString }}|{{ .singlebaz | toString }}",
+			"other":   `{{ .foo | toString }}|{{ .bar | toString }}`,
+		},
+	}
+	tc.ExpectedSecret.Data = map[string][]byte{
+		"my-data": []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{
+			"my-data":   "executed: {{ .singlefoo }}|{{ .singlebaz }}",
+			"other":     `{{ .foo }}|{{ .bar }}`,
+			"sprig-str": `{{ .foo | upper }}`,
+			"json-ex":   `{{ $var := .singlejson | fromJson }}{{ $var.foo | toJson }}`,
+		},
+	}
+	tc.ExpectedSecret.Data = map[string][]byte{
+		"my-data":   []byte(`executed: bar|bang`),
+		"other":     []byte(`barmap|bangmap`),
+		"sprig-str": []byte(`BARMAP`),
+		"json-ex":   []byte(`{"bar":"baz"}`),
+	}
+}
+
+// This case uses template engine v1.
+func genericTemplate(f *framework.Framework) (string, func(*framework.TestCase)) {
+	return "[template] should execute template v1", func(tc *framework.TestCase) {
+		tc.ExpectedSecret = &v1.Secret{
+			Type: v1.SecretTypeOpaque,
+		}
+		tc.ExternalSecret.Spec.Data = []esv1alpha1.ExternalSecretData{
+			{
+				SecretKey: "singlefoo",
+				RemoteRef: esv1alpha1.ExternalSecretDataRemoteRef{
+					Key: "foo",
+				},
+			},
+			{
+				SecretKey: "singlebaz",
+				RemoteRef: esv1alpha1.ExternalSecretDataRemoteRef{
+					Key: "baz",
+				},
+			},
+			{
+				SecretKey: "singlejson",
+				RemoteRef: esv1alpha1.ExternalSecretDataRemoteRef{
+					Key: "json",
+				},
+			},
+		}
+		tc.ExternalSecret.Spec.DataFrom = []esv1alpha1.ExternalSecretDataRemoteRef{
+			{
+				Key: "map",
+			},
+		}
+	}
+}

+ 4 - 2
go.mod

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

+ 10 - 4
go.sum

@@ -94,10 +94,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=
@@ -517,11 +517,13 @@ github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKe
 github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 h1:xixZ2bWeofWV68J+x6AzmKuVM/JWCQwkWm6GW/MUR6I=
 github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
 github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw=
 github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
 github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
+github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
 github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
 github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
@@ -759,6 +761,8 @@ github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFo
 github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
 github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
+github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
+github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
 github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
@@ -779,6 +783,7 @@ github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B
 github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
 github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
 github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
 github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
 github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
 github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
@@ -898,6 +903,7 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
 golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20201217014255-9d1352758620/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=

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

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

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

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

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

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

+ 36 - 0
pkg/template/engine.go

@@ -0,0 +1,36 @@
+/*
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+limitations under the License.
+*/
+package template
+
+import (
+	corev1 "k8s.io/api/core/v1"
+
+	esapi "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
+	v1 "github.com/external-secrets/external-secrets/pkg/template/v1"
+	v2 "github.com/external-secrets/external-secrets/pkg/template/v2"
+)
+
+type ExecFunc func(tpl, data map[string][]byte, secret *corev1.Secret) error
+
+func EngineForVersion(version esapi.TemplateEngineVersion) (ExecFunc, error) {
+	switch version {
+	case esapi.TemplateEngineV1:
+		return v1.Execute, nil
+	case esapi.TemplateEngineV2:
+		return v2.Execute, nil
+	}
+
+	// in case we run with a old v1alpha1 CRD
+	// we must return v1 as default
+	return v1.Execute, nil
+}

+ 0 - 2
pkg/template/template.go

@@ -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.

pkg/template/template_test.go → pkg/template/v1/template_test.go


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

@@ -0,0 +1,188 @@
+/*
+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"
+	tpl "text/template"
+
+	"github.com/Masterminds/sprig/v3"
+	"github.com/lestrrat-go/jwx/jwk"
+	"github.com/youmark/pkcs8"
+	"golang.org/x/crypto/pkcs12"
+	corev1 "k8s.io/api/core/v1"
+)
+
+var tplFuncs = tpl.FuncMap{
+	"pkcs12key":      pkcs12key,
+	"pkcs12keyPass":  pkcs12keyPass,
+	"pkcs12cert":     pkcs12cert,
+	"pkcs12certPass": pkcs12certPass,
+
+	"pemPrivateKey":  pemPrivateKey,
+	"pemCertificate": pemCertificate,
+
+	"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"
+	errConvertPrivKey       = "unable to convert pkcs12 private key: %s"
+	errDecodeCertWithPass   = "unable to decode pkcs12 certificate with password: %s"
+	errEncodePEMKey         = "unable to encode pem private key: %s"
+	errEncodePEMCert        = "unable to encode pem certificate: %s"
+)
+
+func init() {
+	fmt.Printf("calling init in v2 pkg")
+	sprigFuncs := sprig.TxtFuncMap()
+	delete(sprigFuncs, "env")
+	delete(sprigFuncs, "expandenv")
+
+	for k, v := range sprigFuncs {
+		fmt.Printf("adding func %s\n", k)
+		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
+}
+
+func pkcs12keyPass(pass, input string) (string, error) {
+	key, _, err := pkcs12.Decode([]byte(input), pass)
+	if err != nil {
+		return "", fmt.Errorf(errDecodePKCS12WithPass, err)
+	}
+	kb, err := pkcs8.ConvertPrivateKeyToPKCS8(key)
+	if err != nil {
+		return "", fmt.Errorf(errConvertPrivKey, err)
+	}
+	return string(kb), nil
+}
+
+func pkcs12key(input string) (string, error) {
+	return pkcs12keyPass("", input)
+}
+
+func pkcs12certPass(pass, input string) (string, error) {
+	_, cert, err := pkcs12.Decode([]byte(input), pass)
+	if err != nil {
+		return "", fmt.Errorf(errDecodeCertWithPass, err)
+	}
+	return string(cert.Raw), nil
+}
+
+func pkcs12cert(input string) (string, error) {
+	return pkcs12certPass("", input)
+}
+
+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")
+}
+
+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
+}
+
+func pemPrivateKey(key string) (string, error) {
+	res, err := pemEncode(key, "PRIVATE KEY")
+	if err != nil {
+		return res, fmt.Errorf(errEncodePEMKey, err)
+	}
+	return res, nil
+}
+
+func pemCertificate(cert string) (string, error) {
+	res, err := pemEncode(cert, "CERTIFICATE")
+	if err != nil {
+		return res, fmt.Errorf(errEncodePEMCert, err)
+	}
+	return res, nil
+}

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