-
Notifications
You must be signed in to change notification settings - Fork 50
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Custom secret template for ServiceBinding (#398)
- Loading branch information
Showing
16 changed files
with
871 additions
and
66 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
package utils | ||
|
||
import ( | ||
"errors" | ||
"io" | ||
) | ||
|
||
var ( | ||
// ErrLimitExceeded is the error returned when a limit was exceeded. | ||
// Callers will test for ErrLimitExceeded using ==. | ||
ErrLimitExceeded = errors.New("limit exceeded") | ||
) | ||
|
||
// A LimitedWriter writes to W but limits the amount of data that can be written | ||
// to just N bytes. Each call to Write immediately returns ErrLimitExceeded if N | ||
// <= 0. Otherwise Write writes max N bytes to W and updates N to reflect the | ||
// new amount remaining. If the number of byte to be written is greater than N, | ||
// ErrLimitExceeded is returned. Any error from W is returned to the caller of | ||
// Write, i.e. they have precedence over ErrLimitExceeded. | ||
type LimitedWriter struct { | ||
W io.Writer // underlying writer | ||
N int64 // max bytes remaining | ||
Converter func(err error) error | ||
} | ||
|
||
// Write implements io.Writer | ||
func (l *LimitedWriter) Write(p []byte) (int, error) { | ||
if l.N <= 0 { | ||
return l.returnAfterConverter(0, ErrLimitExceeded) | ||
} | ||
|
||
writeable := int64(len(p)) | ||
if l.N < writeable { | ||
writeable = l.N | ||
} | ||
|
||
written, err := l.W.Write(p[:writeable]) | ||
if written < 0 { | ||
written = 0 | ||
} | ||
l.N -= int64(written) | ||
if err != nil { | ||
return l.returnAfterConverter(written, err) | ||
} | ||
if written < int(writeable) { | ||
return l.returnAfterConverter(written, io.ErrShortWrite) | ||
} | ||
|
||
if writeable < int64(len(p)) { | ||
err = ErrLimitExceeded | ||
} | ||
return l.returnAfterConverter(written, err) | ||
} | ||
|
||
func (l *LimitedWriter) returnAfterConverter(written int, err error) (int, error) { | ||
if l.Converter != nil { | ||
err = l.Converter(err) | ||
} | ||
return written, err | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
package utils | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
|
||
. "github.com/onsi/ginkgo" | ||
. "github.com/onsi/gomega" | ||
) | ||
|
||
var _ = Describe("limit writer", func() { | ||
/* tests for limit_writer.go */ | ||
Describe("limited writer", func() { | ||
It("should write to the underlying writer", func() { | ||
// Given | ||
limitedWriter := LimitedWriter{ | ||
W: nil, | ||
N: 0, | ||
Converter: nil, | ||
} | ||
|
||
// When | ||
i, err := limitedWriter.Write(nil) | ||
// Then | ||
Expect(i).To(Equal(0)) | ||
Expect(err).To(Equal(ErrLimitExceeded)) | ||
}) | ||
It("should return error when N <= 0", func() { | ||
// Given | ||
limitedWriter := LimitedWriter{ | ||
W: nil, | ||
N: 0, | ||
Converter: func(err error) error { | ||
if errors.Is(err, ErrLimitExceeded) { | ||
return fmt.Errorf("the size of the generated secret manifest exceeds the limit of %d bytes", 0) | ||
} | ||
return err | ||
}, | ||
} | ||
|
||
// When | ||
i, err := limitedWriter.Write(nil) | ||
|
||
// Then | ||
Expect(i).To(Equal(0)) | ||
Expect(err.Error()).To(ContainSubstring("the size of the generated secret manifest exceeds the limit of 0 bytes")) | ||
|
||
}) | ||
It("should return error when N < writeable", func() { | ||
// Given | ||
limitedWriter := LimitedWriter{ | ||
W: nil, | ||
N: 0, | ||
Converter: nil, | ||
} | ||
|
||
// When | ||
limitedWriter.Write(nil) | ||
|
||
// Then | ||
}) | ||
|
||
}) | ||
|
||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
package utils | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"text/template" | ||
|
||
"github.com/SAP/sap-btp-service-operator/api/common" | ||
|
||
"k8s.io/apimachinery/pkg/runtime/schema" | ||
|
||
"sigs.k8s.io/yaml" | ||
|
||
"github.com/pkg/errors" | ||
corev1 "k8s.io/api/core/v1" | ||
) | ||
|
||
const templateOutputMaxBytes int64 = 1 * 1024 * 1024 | ||
|
||
var allowedMetadataFields = map[string]string{"labels": "any", "annotations": "any", "creationTimestamp": "any"} | ||
var validGroupVersionKind = schema.GroupVersionKind{ | ||
Group: "", | ||
Kind: "Secret", | ||
Version: "v1", | ||
} | ||
|
||
// CreateSecretFromTemplate executes the template to create a secret objects, validates and returns it | ||
// The template needs to be a v1 Secret and in metadata labels and annotations are allowed only | ||
// Set templateOptions of the "text/template" package to specify the template behavior | ||
func CreateSecretFromTemplate(templateName, secretTemplate string, option string, data map[string]interface{}) (*corev1.Secret, error) { | ||
|
||
secretManifest, err := executeTemplate(templateName, secretTemplate, option, data) | ||
if err != nil { | ||
return nil, errors.Wrap(err, "the Secret template is invalid") | ||
} | ||
|
||
secret := &corev1.Secret{} | ||
if err := yaml.Unmarshal(secretManifest, secret); err != nil { | ||
return nil, errors.Wrap(err, "the Secret template is invalid: It does not result in a valid Secret YAML") | ||
} | ||
|
||
if err := validateSecret(secret); err != nil { | ||
return nil, err | ||
} | ||
return secret, nil | ||
} | ||
|
||
func validateSecret(secret *corev1.Secret) error { | ||
// validate GroupVersionKind | ||
gvk := secret.GetObjectKind().GroupVersionKind() | ||
if (gvk.Kind != "" || gvk.Version != "") && gvk != validGroupVersionKind { | ||
return fmt.Errorf("the Secret template is invalid: It is of kind '%s' but needs to be of kind 'Secret'", gvk.String()) | ||
} | ||
|
||
metadataKeyValues := map[string]interface{}{} | ||
secretMetadataBytes, err := json.Marshal(secret.ObjectMeta) | ||
if err != nil { | ||
return errors.Wrap(err, "the Secret template is invalid: It does not result in a valid Secret YAML") | ||
} | ||
if err := json.Unmarshal(secretMetadataBytes, &metadataKeyValues); err != nil { | ||
return errors.Wrap(err, "the Secret template is invalid: It does not result in a valid Secret YAML") | ||
} | ||
|
||
for metadataKey := range metadataKeyValues { | ||
if _, ok := allowedMetadataFields[metadataKey]; !ok { | ||
return fmt.Errorf("the Secret template is invalid: Secret's metadata field '%s' cannot be edited", metadataKey) | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// ParseTemplate create a new template with given name, add allowed sprig functions and parse the template | ||
func ParseTemplate(templateName, text string) (*template.Template, error) { | ||
return template.New(templateName).Funcs(filteredFuncMap()).Parse(text) | ||
} | ||
|
||
func filteredFuncMap() template.FuncMap { | ||
|
||
return template.FuncMap{} | ||
} | ||
|
||
func executeTemplate(templateName, text, option string, parameters map[string]interface{}) ([]byte, error) { | ||
t, err := ParseTemplate(templateName, text) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
var buf bytes.Buffer | ||
var writer io.Writer = &LimitedWriter{ | ||
W: &buf, | ||
N: templateOutputMaxBytes, | ||
Converter: func(err error) error { | ||
if errors.Is(err, ErrLimitExceeded) { | ||
return fmt.Errorf("the size of the generated Secret exceeds the limit of %d bytes", templateOutputMaxBytes) | ||
} | ||
return err | ||
}, | ||
} | ||
err = t.Option(option).Execute(writer, parameters) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return buf.Bytes(), nil | ||
} | ||
|
||
func GetSecretDataForTemplate(Credential map[string]interface{}, instance map[string]string) map[string]interface{} { | ||
return map[string]interface{}{ | ||
common.CredentialsKey: Credential, | ||
common.InstanceKey: instance, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
package utils | ||
|
||
import ( | ||
"fmt" | ||
"strings" | ||
|
||
"github.com/lithammer/dedent" | ||
. "github.com/onsi/ginkgo" | ||
. "github.com/onsi/gomega" | ||
corev1 "k8s.io/api/core/v1" | ||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
) | ||
|
||
var _ = Describe("Secret template", func() { | ||
|
||
Context("With valid secretTemplate, but missing keys (credentials are nil)", func() { | ||
|
||
It("should fail", func() { | ||
nonexistingKey := "nonexistingKey" | ||
secretTemplate := fmt.Sprintf( | ||
dedent.Dedent(` | ||
apiVersion: v1 | ||
kind: Secret | ||
stringData: | ||
foo: {{ .%s }} | ||
`), | ||
nonexistingKey, | ||
) | ||
|
||
secret, err := CreateSecretFromTemplate("", secretTemplate, "missingkey=error", nil) | ||
|
||
Expect(err).Should(MatchError(ContainSubstring("map has no entry for key \"%s\"", nonexistingKey))) | ||
Expect(secret).Should(BeNil()) | ||
}) | ||
}) | ||
|
||
Context("With unknown field", func() { | ||
|
||
It("should succeed and invalid key provided in the secret is ignored", func() { | ||
expectedSecret := &corev1.Secret{ | ||
TypeMeta: v1.TypeMeta{ | ||
Kind: "Secret", | ||
APIVersion: "v1", | ||
}, | ||
} | ||
secretTemplate := dedent.Dedent(` | ||
apiVersion: v1 | ||
kind: Secret | ||
unknownField: foo | ||
`) | ||
|
||
secret, err := CreateSecretFromTemplate("", secretTemplate, "missingkey=error", nil) | ||
|
||
Expect(err).ShouldNot(HaveOccurred()) | ||
Expect(secret).Should(Equal(expectedSecret)) | ||
}) | ||
}) | ||
|
||
Context("With wrong kind", func() { | ||
|
||
It("should fail", func() { | ||
secretTemplate := dedent.Dedent(` | ||
apiVersion: v1 | ||
kind: Pod | ||
`) | ||
|
||
secret, err := CreateSecretFromTemplate("", secretTemplate, "missingkey=error", nil) | ||
|
||
Expect(err).Should(MatchError( | ||
SatisfyAll( | ||
ContainSubstring("generated secret manifest has unexpected type"), | ||
ContainSubstring("Pod"), | ||
), | ||
)) | ||
Expect(secret).Should(BeNil()) | ||
}) | ||
}) | ||
|
||
Context("With sprig functions", func() { | ||
|
||
It("should fail if forbidden sprig func is used in the template", func() { | ||
secretTemplate := dedent.Dedent(` | ||
apiVersion: v1 | ||
kind: Secret | ||
stringData: | ||
foo: {{ .param1 | env }} | ||
`) | ||
|
||
secret, err := CreateSecretFromTemplate("", secretTemplate, "missingkey=error", nil) | ||
|
||
Expect(err).Should(MatchError(ContainSubstring("function \"env\" not defined"))) | ||
Expect(secret).To(BeNil()) | ||
}) | ||
|
||
Describe("limited template output size", func() { | ||
|
||
It("should succeed if template output is too big", func() { | ||
secretTemplate := dedent.Dedent(` | ||
apiVersion: v1 | ||
kind: Secret | ||
stringData: | ||
foo: x | ||
`) | ||
secretTemplate += strings.Repeat("#", int(templateOutputMaxBytes)-len(secretTemplate)) | ||
Expect(len(secretTemplate)).To(Equal(int(templateOutputMaxBytes))) | ||
|
||
secret, err := CreateSecretFromTemplate("", secretTemplate, "missingkey=error", nil) | ||
|
||
Expect(err).ShouldNot(HaveOccurred()) | ||
Expect(secret).NotTo(BeNil()) | ||
}) | ||
|
||
It("should fail if template output is too big", func() { | ||
secretTemplate := strings.Repeat("a", int(templateOutputMaxBytes)+1) | ||
|
||
secret, err := CreateSecretFromTemplate("", secretTemplate, "missingkey=error", nil) | ||
|
||
Expect(err).Should(MatchError(ContainSubstring("the size of the generated secret manifest exceeds the limit"))) | ||
Expect(secret).To(BeNil()) | ||
}) | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.