Skip to content
This repository has been archived by the owner on Jun 26, 2024. It is now read-only.

Rebind workloads when their mappings change #1296

Merged
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
4 changes: 3 additions & 1 deletion apis/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,20 @@ package apis
import (
"encoding/json"
"errors"
"reflect"

"k8s.io/api/authentication/v1"
authv1 "k8s.io/api/authentication/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"reflect"
"sigs.k8s.io/controller-runtime/pkg/client"
)

const (
finalizerName = "finalizer.servicebinding.openshift.io"
requesterAnnotationKey = "servicebinding.io/requester"
MappingAnnotationKey = "servicebinding.io/mapping"
)

func MaybeAddFinalizer(obj Object) bool {
Expand Down
26 changes: 19 additions & 7 deletions apis/spec/v1beta1/clusterworkloadresourcemapping_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,12 @@ import (
"strings"

"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/client-go/util/jsonpath"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/webhook"
)

func (r *ClusterWorkloadResourceMapping) SetupWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr).
For(r).
Complete()
}

// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!

// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
Expand Down Expand Up @@ -145,3 +139,21 @@ func verifyJsonPath(node jsonpath.Node, path *field.Path, value string) field.Er
}
return errs
}

func (mapping *ClusterWorkloadResourceMapping) AcceptsGVR(gvk *schema.GroupVersionResource) bool {
expectedName := fmt.Sprintf("%v.%v", gvk.Resource, gvk.Group)
if mapping.Name != expectedName {
return false
}
for _, version := range mapping.Spec.Versions {
switch version.Version {
case gvk.Version:
return true
case "*":
return true
default:
continue
}
}
return false
}
48 changes: 48 additions & 0 deletions apis/spec/v1beta1/clusterworkloadresourcemapping_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/ginkgo/extensions/table"
. "github.com/onsi/gomega"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/validation/field"
)

Expand Down Expand Up @@ -220,3 +222,49 @@ var _ = Describe("Validation Webhook", func() {
Expect(mapping.validate()).To(BeNil())
})
})

var _ = Describe("AcceptGVR", func() {
var (
mapping ClusterWorkloadResourceMapping
gvr schema.GroupVersionResource
)

BeforeEach(func() {
mapping = ClusterWorkloadResourceMapping{
ObjectMeta: v1.ObjectMeta{
Name: "foos.bar",
},
Spec: ClusterWorkloadResourceMappingSpec{
Versions: []ClusterWorkloadResourceMappingTemplate{
{
Version: "v1",
},
},
},
}
gvr = schema.GroupVersionResource{
Group: "bar",
Resource: "foos",
Version: "v1",
}
})

It("should accept a compatible GVR", func() {
Expect(mapping.AcceptsGVR(&gvr)).To(BeTrue())
})

It("should accept when matching a wildcard template", func() {
mapping.Spec.Versions[0].Version = "*"
Expect(mapping.AcceptsGVR(&gvr)).To(BeTrue())
})

It("should reject an incompatible GVR", func() {
mapping.Spec.Versions[0].Version = "v2"
Expect(mapping.AcceptsGVR(&gvr)).To(BeFalse())
})

It("should ignore GVRs with an incompatible Group/Resource", func() {
mapping.Name = "spams.bar"
Expect(mapping.AcceptsGVR(&gvr)).To(BeFalse())
})
})
13 changes: 13 additions & 0 deletions apis/webhooks/suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package webhooks_test

import (
"testing"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

func TestBindingHandlers(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Binding Handlers Suite")
}
175 changes: 175 additions & 0 deletions apis/webhooks/validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package webhooks

import (
"context"
"encoding/json"

"github.com/redhat-developer/service-binding-operator/apis"
"github.com/redhat-developer/service-binding-operator/apis/binding/v1alpha1"
"github.com/redhat-developer/service-binding-operator/apis/spec/v1beta1"
"github.com/redhat-developer/service-binding-operator/pkg/client/kubernetes"
"k8s.io/apimachinery/pkg/api/meta"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
ctrl "sigs.k8s.io/controller-runtime"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook"
)

type MappingValidator struct {
client dynamic.Interface
lookup kubernetes.K8STypeLookup
}

// log is for logging in this package.
var log = logf.Log.WithName("WebHook Spec ClusterWorkloadResourceMapping")

func NewMappingValidator(config *rest.Config, mapper meta.RESTMapper) (*MappingValidator, error) {
client, err := dynamic.NewForConfig(config)
if err != nil {
return nil, err
}
validator := &MappingValidator{
client: client,
lookup: kubernetes.ResourceLookup(mapper),
}

return validator, nil
}

func (validator *MappingValidator) SetupWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr).
For(&v1beta1.ClusterWorkloadResourceMapping{}).
WithValidator(validator).
Complete()
}

var _ webhook.CustomValidator = &MappingValidator{}

func (validator *MappingValidator) ValidateCreate(ctx context.Context, obj runtime.Object) error {
mapping := obj.(*v1beta1.ClusterWorkloadResourceMapping)
err := mapping.ValidateCreate()
if err != nil {
log.Error(err, "Error validating mapping (create)", "mapping", mapping.Name)
}
return err
}

func (validator *MappingValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) error {
mapping := newObj.(*v1beta1.ClusterWorkloadResourceMapping)
if err := mapping.ValidateCreate(); err != nil {
log.Error(err, "Error validating mapping (update)", "mapping", mapping.Name)
return err
}
oldMapping := oldObj.(*v1beta1.ClusterWorkloadResourceMapping)
err := Serialize(ctx, oldMapping, validator.client, validator.lookup)
if err != nil {
return err
}

return nil
}

func (validator *MappingValidator) ValidateDelete(ctx context.Context, obj runtime.Object) error {
return nil
}

func Serialize(ctx context.Context, mapping *v1beta1.ClusterWorkloadResourceMapping, client dynamic.Interface, lookup kubernetes.K8STypeLookup) error {
serialized, err := json.Marshal(mapping)
if err != nil {
return err
}
numItems := 0

gvr := v1beta1.GroupVersionResource
data, err := client.Resource(gvr).List(ctx, v1.ListOptions{})
if err != nil {
return err
}

for _, binding := range data.Items {
// we should filter out service bindings that the mapping doesn't affect.
sb := v1beta1.ServiceBinding{}
err = runtime.DefaultUnstructuredConverter.FromUnstructured(binding.Object, &sb)
if err != nil {
// short circuit, something's gone terribly wrong
return err
}

gvk, _ := sb.Spec.Workload.GroupVersionKind()
workloadGVR, err := lookup.ResourceForKind(*gvk)
if err != nil {
return err
}
if !mapping.AcceptsGVR(workloadGVR) {
// not a relevant binding, skip it
continue
}

annotations := binding.GetAnnotations()
if annotations == nil {
annotations = map[string]string{
apis.MappingAnnotationKey: string(serialized),
}
} else {
annotations[apis.MappingAnnotationKey] = string(serialized)
}
binding.SetAnnotations(annotations)

x, err := client.Resource(gvr).Namespace(sb.Namespace).Update(ctx, &binding, v1.UpdateOptions{})
if err != nil {
return err
}
log.Info("deployed service binding", "annotations", x.GetAnnotations())
numItems += 1
}

gvr = v1alpha1.GroupVersionResource
data, err = client.Resource(gvr).List(ctx, v1.ListOptions{})
if err != nil {
return err
}

for _, binding := range data.Items {
sb := v1alpha1.ServiceBinding{}
err = runtime.DefaultUnstructuredConverter.FromUnstructured(binding.Object, &sb)
if err != nil {
return err
}

var workloadGVR *schema.GroupVersionResource
if sb.Spec.Application.Kind != "" {
gvk, _ := sb.Spec.Application.GroupVersionKind()
if workloadGVR, err = lookup.ResourceForKind(*gvk); err != nil {
return err
}
} else {
workloadGVR, _ = sb.Spec.Application.GroupVersionResource()
}
if !mapping.AcceptsGVR(workloadGVR) {
continue
}

annotations := binding.GetAnnotations()
if annotations == nil {
annotations = map[string]string{
apis.MappingAnnotationKey: string(serialized),
}
} else {
annotations[apis.MappingAnnotationKey] = string(serialized)
}
binding.SetAnnotations(annotations)

x, err := client.Resource(gvr).Namespace(sb.Namespace).Update(ctx, &binding, v1.UpdateOptions{})
if err != nil {
return err
}
log.Info("deployed service binding", "annotations", x.GetAnnotations())
numItems += 1
}
log.Info("Rebinding", "num_objects", numItems)
return nil
}
Loading