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

Commit

Permalink
Rebind workloads when their mappings change
Browse files Browse the repository at this point in the history
Signed-off-by: Andy Sadler <[email protected]>
  • Loading branch information
sadlerap committed Nov 3, 2022
1 parent a6d68cb commit 5288dcb
Show file tree
Hide file tree
Showing 18 changed files with 1,177 additions and 238 deletions.
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
}
49 changes: 49 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,50 @@ 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.Spec.Versions[0].Version = "v1"
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

0 comments on commit 5288dcb

Please sign in to comment.