Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow a custom structure of Kubernetes secrets by introducing secretTemplate to serviceBindingsadd secret template #373

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ jobs:
runs-on: ubuntu-latest
steps:

- name: Set up Go 1.20.0
- name: Set up Go 1.21.4
uses: actions/setup-go@v2
with:
go-version: 1.20.0
go-version: 1.21.4

- name: Check out code into the Go module directory
uses: actions/checkout@v2
Expand All @@ -31,10 +31,10 @@ jobs:

- name: Test
run: make test

- name: Lint
run: make lint

- name: Send coverage
uses: shogo82148/actions-goveralls@v1
with:
Expand Down
254 changes: 183 additions & 71 deletions README.md

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions api/v1/servicebinding_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,17 @@ type ServiceBindingSpec struct {
// CredentialsRotationPolicy holds automatic credentials rotation configuration.
// +optional
CredRotationPolicy *CredentialsRotationPolicy `json:"credentialsRotationPolicy,omitempty"`

// SecretTemplate is a Go template that generates a custom Kubernetes
// v1/Secret based on the data of the service binding returned by
// Service Manager.
// The generated secret is used instead of the default secret.
// This is useful if the consumer of service binding data expects them in
// a specific format.
// For Go templates see https://pkg.go.dev/text/template.
// +optional
// +kubebuilder:pruning:PreserveUnknownFields
SecretTemplate string `json:"secretTemplate,omitempty"`
}

// ServiceBindingStatus defines the observed state of ServiceBinding
Expand Down
14 changes: 14 additions & 0 deletions api/v1/servicebinding_validating_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@ import (
"reflect"
"time"

"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

"github.com/SAP/sap-btp-service-operator/api"
"github.com/SAP/sap-btp-service-operator/internal/secrets/template"
)

// log is for logging in this package.
Expand All @@ -54,6 +56,11 @@ func (sb *ServiceBinding) ValidateCreate() (admission.Warnings, error) {
return nil, err
}
}
if sb.Spec.SecretTemplate != "" {
if err := sb.validateSecretTemplate(); err != nil {
return nil, errors.Wrap(err, "spec.secretTemplate is invalid")
}
}
return nil, nil
}

Expand Down Expand Up @@ -124,3 +131,10 @@ func (sb *ServiceBinding) validateCredRotatingConfig() error {

return nil
}

func (sb *ServiceBinding) validateSecretTemplate() error {
servicebindinglog.Info("validate specified secretTemplate")

_, err := template.ParseTemplate("", sb.Spec.SecretTemplate)
return err
}
37 changes: 37 additions & 0 deletions api/v1/servicebinding_validating_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package v1

import (
"github.com/SAP/sap-btp-service-operator/api"
"github.com/lithammer/dedent"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"k8s.io/apimachinery/pkg/runtime"
Expand All @@ -19,6 +20,29 @@ var _ = Describe("Service Binding Webhook Test", func() {
_, err := binding.ValidateCreate()
Expect(err).ToNot(HaveOccurred())
})
It("should succeed if secretTemplate can be parsed", func() {
binding.Spec.SecretTemplate = dedent.Dedent(`
apiVersion: v1
kind: Secret
stringData:
secretKey: {{ .secretValue | quote }}`)

_, err := binding.ValidateCreate()

Expect(err).ToNot(HaveOccurred())
})

It("should fail if secretTemplate cannot be parsed", func() {
binding.Spec.SecretTemplate = dedent.Dedent(`
apiVersion: v1
kind: Secret
stringData:
secretKey: {{ .secretValue | quote`)

_, err := binding.ValidateCreate()

Expect(err).Should(MatchError(ContainSubstring("spec.secretTemplate is invalid")))
})
})

Context("Validate update of spec before binding is created (failure recovery)", func() {
Expand Down Expand Up @@ -61,6 +85,19 @@ var _ = Describe("Service Binding Webhook Test", func() {
Expect(err).ToNot(HaveOccurred())
})
})

When("SecretTemplate changed", func() {
It("should succeed", func() {
modifiedSecretTemplate := `
apiVersion: v1
kind: Secret
stringData:
key2: "value2"`
newBinding.Spec.SecretTemplate = modifiedSecretTemplate
_, err := newBinding.ValidateUpdate(binding)
Expect(err).ToNot(HaveOccurred())
})
})
})

When("Metadata changed", func() {
Expand Down
6 changes: 6 additions & 0 deletions api/v1/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package v1
import (
"testing"

"github.com/lithammer/dedent"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
v1 "k8s.io/api/authentication/v1"
Expand Down Expand Up @@ -47,6 +48,11 @@ func getBinding() *ServiceBinding {
RotationFrequency: "1s",
RotatedBindingTTL: "1s",
},
SecretTemplate: dedent.Dedent(`
apiVersion: v1
kind: Secret
stringData:
key1: value1`),
},

Status: ServiceBindingStatus{},
Expand Down
8 changes: 8 additions & 0 deletions config/crd/bases/services.cloud.sap.com_servicebindings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,14 @@ spec:
and additional info under single key. Convenient way to store whole
binding data in single file when using `volumeMounts`.
type: string
secretTemplate:
description: SecretTemplate is a Go template that generates a custom
Kubernetes v1/Secret based on the data of the service binding returned
by Service Manager. The generated secret is used instead of the
default secret. This is useful if the consumer of service binding
data expects them in a specific format. For Go templates see https://pkg.go.dev/text/template.
type: string
x-kubernetes-preserve-unknown-fields: true
serviceInstanceName:
description: The k8s name of the service instance to bind, should
be in the namespace of the binding
Expand Down
89 changes: 77 additions & 12 deletions controllers/servicebinding_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import (
"sigs.k8s.io/controller-runtime/pkg/controller"

servicesv1 "github.com/SAP/sap-btp-service-operator/api/v1"
"github.com/SAP/sap-btp-service-operator/internal/secrets/template"
"github.com/pkg/errors"

"github.com/SAP/sap-btp-service-operator/api"

Expand All @@ -49,8 +51,10 @@ import (
)

const (
secretNameTakenErrorFormat = "the specified secret name '%s' is already taken. Choose another name and try again"
secretAlreadyOwnedErrorFormat = "secret %s belongs to another binding %s, choose a different name"
secretNameTakenErrorFormat = "the specified secret name '%s' is already taken. Choose another name and try again"
secretAlreadyOwnedErrorFormat = "secret %s belongs to another binding %s, choose a different name"
secretTemplateSmBindingKey = "smBindingCredentials"
secretTemplateServiceInstanceInfos = "serviceInstanceInfos"
)

// ServiceBindingReconciler reconciles a ServiceBinding object
Expand Down Expand Up @@ -538,16 +542,81 @@ func (r *ServiceBindingReconciler) resyncBindingStatus(ctx context.Context, k8sB
func (r *ServiceBindingReconciler) storeBindingSecret(ctx context.Context, k8sBinding *servicesv1.ServiceBinding, smBinding *smClientTypes.ServiceBinding) error {
log := GetLogger(ctx)
logger := log.WithValues("bindingName", k8sBinding.Name, "secretName", k8sBinding.Spec.SecretName)
var secret *corev1.Secret
var err error

if k8sBinding.Spec.SecretTemplate != "" {
secret, err = r.createBindingSecretFromSecretTemplate(ctx, k8sBinding, smBinding.Credentials)
} else {
secret, err = r.createBindingSecret(ctx, k8sBinding, smBinding.Credentials)
}

if err != nil {
return err
}

if err := controllerutil.SetControllerReference(k8sBinding, secret, r.Scheme); err != nil {
logger.Error(err, "Failed to set secret owner")
return err
}

return r.createOrUpdateBindingSecret(ctx, k8sBinding, secret)
}

// createBindingSecretFromSecretTemplate executes the template of .Spec.SecretTemplate
func (r *ServiceBindingReconciler) createBindingSecretFromSecretTemplate(ctx context.Context, k8sBinding *servicesv1.ServiceBinding, inputSmCredentials json.RawMessage) (*corev1.Secret, error) {
log := GetLogger(ctx)
log.Info("Create Object using SecretTemplate from ServiceBinding Specs")

smBindingCredentials := make(map[string]interface{})
if inputSmCredentials != nil {
err := json.Unmarshal(inputSmCredentials, &smBindingCredentials)
if err != nil {
return nil, errors.Wrap(err, "failed to unmarshal given service binding credentials")
}
}

instanceInfos := make(map[string][]byte)
_, err := r.addInstanceInfo(ctx, k8sBinding, instanceInfos)
if err != nil {
return nil, errors.Wrap(err, "failed to add service instance info")
}

//convert the bytes to string to ensure, that the secret can be created later by CreateSecretFromTemplate
convertedInstanceInfos := make(map[string]string)
for k, v := range instanceInfos {
convertedInstanceInfos[k] = string(v)
}

parameters := map[string]interface{}{
secretTemplateSmBindingKey: smBindingCredentials,
secretTemplateServiceInstanceInfos: convertedInstanceInfos,
}

templateName := fmt.Sprintf("%s/%s", k8sBinding.Namespace, k8sBinding.Name)
secret, err := template.CreateSecretFromTemplate(templateName, k8sBinding.Spec.SecretTemplate, parameters)
if err != nil {
return nil, errors.Wrap(err, "failed to create secret from template")
}

secret.SetNamespace(k8sBinding.Namespace)
secret.SetName(k8sBinding.Spec.SecretName)

return secret, nil
}

func (r *ServiceBindingReconciler) createBindingSecret(ctx context.Context, k8sBinding *servicesv1.ServiceBinding, credentials json.RawMessage) (*corev1.Secret, error) {
log := GetLogger(ctx)
logger := log.WithValues("bindingName", k8sBinding.Name, "secretName", k8sBinding.Spec.SecretName)
var credentialsMap map[string][]byte
var credentialProperties []SecretMetadataProperty

if len(smBinding.Credentials) == 0 {
if len(credentials) == 0 {
log.Info("Binding credentials are empty")
credentialsMap = make(map[string][]byte)
} else if k8sBinding.Spec.SecretKey != nil {
credentialsMap = map[string][]byte{
*k8sBinding.Spec.SecretKey: smBinding.Credentials,
*k8sBinding.Spec.SecretKey: credentials,
}
credentialProperties = []SecretMetadataProperty{
{
Expand All @@ -558,10 +627,10 @@ func (r *ServiceBindingReconciler) storeBindingSecret(ctx context.Context, k8sBi
}
} else {
var err error
credentialsMap, credentialProperties, err = normalizeCredentials(smBinding.Credentials)
credentialsMap, credentialProperties, err = normalizeCredentials(credentials)
if err != nil {
logger.Error(err, "Failed to store binding secret")
return fmt.Errorf("failed to create secret. Error: %v", err.Error())
return nil, fmt.Errorf("failed to create secret. Error: %v", err.Error())
}
}

Expand All @@ -574,7 +643,7 @@ func (r *ServiceBindingReconciler) storeBindingSecret(ctx context.Context, k8sBi
var err error
credentialsMap, err = singleKeyMap(credentialsMap, *k8sBinding.Spec.SecretRootKey)
if err != nil {
return err
return nil, err
}
} else {
metadata := map[string][]SecretMetadataProperty{
Expand All @@ -597,12 +666,8 @@ func (r *ServiceBindingReconciler) storeBindingSecret(ctx context.Context, k8sBi
},
Data: credentialsMap,
}
if err := controllerutil.SetControllerReference(k8sBinding, secret, r.Scheme); err != nil {
logger.Error(err, "Failed to set secret owner")
return err
}

return r.createOrUpdateBindingSecret(ctx, k8sBinding, secret)
return secret, nil
}

func (r *ServiceBindingReconciler) createOrUpdateBindingSecret(ctx context.Context, binding *servicesv1.ServiceBinding, secret *corev1.Secret) error {
Expand Down
Loading
Loading