Skip to content

Commit

Permalink
Add lifecycle hooks
Browse files Browse the repository at this point in the history
The hooks allow defining a custom Resolver or Projector as well as
interacting with the ServiceBinding and workload resources before before
and after projection.

Defining no hooks is equivalent to the current RI behavior.

The controller operates on a single ServiceBinding and zero-to-many
workloads in a single request, while the webhook operates on a single
workload with zero-to-many ServiceBindings. Because of this mismatch,
we split out interactions with the ServiceBinding and workload resources
into independent hooks.

Hooks to migrate from the VMware implementation are available opt-in by
passing the --migrate-from-vmware flag to the manager container. Running
with the migration hooks installed will incur a performance penalty and
should only be used while a migration is active, and then disabled.

Signed-off-by: Scott Andrews <[email protected]>
  • Loading branch information
scothis committed Aug 25, 2023
1 parent 170cfaf commit 1e04b18
Show file tree
Hide file tree
Showing 10 changed files with 478 additions and 22 deletions.
18 changes: 18 additions & 0 deletions apis/duck/groupversion_info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
Copyright 2023 the original author or authors.
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.
*/

// +kubebuilder:object:generate=true
package duck
42 changes: 42 additions & 0 deletions apis/duck/podspecable_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
Copyright 2023 the original author or authors.
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 duck

import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
)

type PodSpecable struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec PodSpecableSpec `json:"spec,omitempty"`
}

type PodSpecableSpec struct {
Template corev1.PodTemplateSpec `json:"template,omitempty"`
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *PodSpecable) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
55 changes: 55 additions & 0 deletions apis/duck/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 33 additions & 12 deletions controllers/servicebinding_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/reconcile"

servicebindingv1beta1 "github.com/servicebinding/runtime/apis/v1beta1"
"github.com/servicebinding/runtime/projector"
"github.com/servicebinding/runtime/resolver"
"github.com/servicebinding/runtime/lifecycle"
)

//+kubebuilder:rbac:groups=servicebinding.io,resources=servicebindings,verbs=get;list;watch;create;update;patch;delete
Expand All @@ -42,14 +41,14 @@ import (
//+kubebuilder:rbac:groups=core,resources=events,verbs=get;list;watch;create;update;patch;delete

// ServiceBindingReconciler reconciles a ServiceBinding object
func ServiceBindingReconciler(c reconcilers.Config) *reconcilers.ResourceReconciler[*servicebindingv1beta1.ServiceBinding] {
func ServiceBindingReconciler(c reconcilers.Config, hooks lifecycle.ServiceBindingHooks) *reconcilers.ResourceReconciler[*servicebindingv1beta1.ServiceBinding] {
return &reconcilers.ResourceReconciler[*servicebindingv1beta1.ServiceBinding]{
Reconciler: &reconcilers.WithFinalizer[*servicebindingv1beta1.ServiceBinding]{
Finalizer: servicebindingv1beta1.GroupVersion.Group + "/finalizer",
Reconciler: reconcilers.Sequence[*servicebindingv1beta1.ServiceBinding]{
ResolveBindingSecret(),
ResolveWorkloads(),
ProjectBinding(),
ResolveBindingSecret(hooks),
ResolveWorkloads(hooks),
ProjectBinding(hooks),
PatchWorkloads(),
},
},
Expand All @@ -58,7 +57,7 @@ func ServiceBindingReconciler(c reconcilers.Config) *reconcilers.ResourceReconci
}
}

func ResolveBindingSecret() reconcilers.SubReconciler[*servicebindingv1beta1.ServiceBinding] {
func ResolveBindingSecret(hooks lifecycle.ServiceBindingHooks) reconcilers.SubReconciler[*servicebindingv1beta1.ServiceBinding] {
return &reconcilers.SyncReconciler[*servicebindingv1beta1.ServiceBinding]{
Name: "ResolveBindingSecret",
Sync: func(ctx context.Context, resource *servicebindingv1beta1.ServiceBinding) error {
Expand All @@ -70,7 +69,7 @@ func ResolveBindingSecret() reconcilers.SubReconciler[*servicebindingv1beta1.Ser
Namespace: resource.Namespace,
Name: resource.Spec.Service.Name,
}
secretName, err := resolver.New(TrackingClient(c)).LookupBindingSecret(ctx, ref)
secretName, err := hooks.GetResolver(TrackingClient(c)).LookupBindingSecret(ctx, ref)
if err != nil {
if apierrs.IsNotFound(err) {
// leave Unknown, the provisioned service may be created shortly
Expand Down Expand Up @@ -116,7 +115,7 @@ func ResolveBindingSecret() reconcilers.SubReconciler[*servicebindingv1beta1.Ser
}
}

func ResolveWorkloads() reconcilers.SubReconciler[*servicebindingv1beta1.ServiceBinding] {
func ResolveWorkloads(hooks lifecycle.ServiceBindingHooks) reconcilers.SubReconciler[*servicebindingv1beta1.ServiceBinding] {
return &reconcilers.SyncReconciler[*servicebindingv1beta1.ServiceBinding]{
Name: "ResolveWorkloads",
SyncDuringFinalization: true,
Expand All @@ -129,7 +128,7 @@ func ResolveWorkloads() reconcilers.SubReconciler[*servicebindingv1beta1.Service
Namespace: resource.Namespace,
Name: resource.Spec.Workload.Name,
}
workloads, err := resolver.New(TrackingClient(c)).LookupWorkloads(ctx, ref, resource.Spec.Workload.Selector)
workloads, err := hooks.GetResolver(TrackingClient(c)).LookupWorkloads(ctx, ref, resource.Spec.Workload.Selector)
if err != nil {
if apierrs.IsNotFound(err) {
// leave Unknown, the workload may be created shortly
Expand Down Expand Up @@ -161,19 +160,30 @@ func ResolveWorkloads() reconcilers.SubReconciler[*servicebindingv1beta1.Service

//+kubebuilder:rbac:groups=servicebinding.io,resources=clusterworkloadresourcemappings,verbs=get;list;watch

func ProjectBinding() reconcilers.SubReconciler[*servicebindingv1beta1.ServiceBinding] {
func ProjectBinding(hooks lifecycle.ServiceBindingHooks) reconcilers.SubReconciler[*servicebindingv1beta1.ServiceBinding] {
return &reconcilers.SyncReconciler[*servicebindingv1beta1.ServiceBinding]{
Name: "ProjectBinding",
SyncDuringFinalization: true,
Sync: func(ctx context.Context, resource *servicebindingv1beta1.ServiceBinding) error {
c := reconcilers.RetrieveConfigOrDie(ctx)
projector := projector.New(resolver.New(TrackingClient(c)))
projector := hooks.GetProjector(hooks.GetResolver(TrackingClient(c)))

workloads := RetrieveWorkloads(ctx)
projectedWorkloads := make([]runtime.Object, len(workloads))

if f := hooks.ServiceBindingPreProjection; f != nil {
if err := f(ctx, resource); err != nil {
return err
}
}
for i := range workloads {
workload := workloads[i].DeepCopyObject()

if f := hooks.WorkloadPreProjection; f != nil {
if err := f(ctx, workload); err != nil {
return err
}
}
if !resource.DeletionTimestamp.IsZero() {
if err := projector.Unproject(ctx, resource, workload); err != nil {
return err
Expand All @@ -183,8 +193,19 @@ func ProjectBinding() reconcilers.SubReconciler[*servicebindingv1beta1.ServiceBi
return err
}
}
if f := hooks.WorkloadPostProjection; f != nil {
if err := f(ctx, workload); err != nil {
return err
}
}

projectedWorkloads[i] = workload
}
if f := hooks.ServiceBindingPostProjection; f != nil {
if err := f(ctx, resource); err != nil {
return err
}
}

StashProjectedWorkloads(ctx, projectedWorkloads)

Expand Down
9 changes: 5 additions & 4 deletions controllers/servicebinding_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import (
servicebindingv1beta1 "github.com/servicebinding/runtime/apis/v1beta1"
"github.com/servicebinding/runtime/controllers"
dieservicebindingv1beta1 "github.com/servicebinding/runtime/dies/v1beta1"
"github.com/servicebinding/runtime/lifecycle"
)

func TestServiceBindingReconciler(t *testing.T) {
Expand Down Expand Up @@ -302,7 +303,7 @@ func TestServiceBindingReconciler(t *testing.T) {
rts.Run(t, scheme, func(t *testing.T, tc *rtesting.ReconcilerTestCase, c reconcilers.Config) reconcile.Reconciler {
restMapper := c.RESTMapper().(*meta.DefaultRESTMapper)
restMapper.Add(schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, meta.RESTScopeNamespace)
return controllers.ServiceBindingReconciler(c)
return controllers.ServiceBindingReconciler(c, lifecycle.ServiceBindingHooks{})
})
}

Expand Down Expand Up @@ -526,7 +527,7 @@ func TestResolveBindingSecret(t *testing.T) {
}

rts.Run(t, scheme, func(t *testing.T, tc *rtesting.SubReconcilerTestCase[*servicebindingv1beta1.ServiceBinding], c reconcilers.Config) reconcilers.SubReconciler[*servicebindingv1beta1.ServiceBinding] {
return controllers.ResolveBindingSecret()
return controllers.ResolveBindingSecret(lifecycle.ServiceBindingHooks{})
})
}

Expand Down Expand Up @@ -760,7 +761,7 @@ func TestResolveWorkload(t *testing.T) {
}

rts.Run(t, scheme, func(t *testing.T, tc *rtesting.SubReconcilerTestCase[*servicebindingv1beta1.ServiceBinding], c reconcilers.Config) reconcilers.SubReconciler[*servicebindingv1beta1.ServiceBinding] {
return controllers.ResolveWorkloads()
return controllers.ResolveWorkloads(lifecycle.ServiceBindingHooks{})
})
}

Expand Down Expand Up @@ -931,7 +932,7 @@ func TestProjectBinding(t *testing.T) {
rts.Run(t, scheme, func(t *testing.T, tc *rtesting.SubReconcilerTestCase[*servicebindingv1beta1.ServiceBinding], c reconcilers.Config) reconcilers.SubReconciler[*servicebindingv1beta1.ServiceBinding] {
restMapper := c.RESTMapper().(*meta.DefaultRESTMapper)
restMapper.Add(schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, meta.RESTScopeNamespace)
return controllers.ProjectBinding()
return controllers.ProjectBinding(lifecycle.ServiceBindingHooks{})
})
}

Expand Down
27 changes: 23 additions & 4 deletions controllers/webhook_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,8 @@ import (
"sigs.k8s.io/controller-runtime/pkg/reconcile"

servicebindingv1beta1 "github.com/servicebinding/runtime/apis/v1beta1"
"github.com/servicebinding/runtime/projector"
"github.com/servicebinding/runtime/lifecycle"
"github.com/servicebinding/runtime/rbac"
"github.com/servicebinding/runtime/resolver"
)

//+kubebuilder:rbac:groups=admissionregistration.k8s.io,resources=mutatingwebhookconfigurations,verbs=get;list;watch;create;update;patch
Expand Down Expand Up @@ -97,7 +96,7 @@ func AdmissionProjectorReconciler(c reconcilers.Config, name string, accessCheck
}
}

func AdmissionProjectorWebhook(c reconcilers.Config) *reconcilers.AdmissionWebhookAdapter[*unstructured.Unstructured] {
func AdmissionProjectorWebhook(c reconcilers.Config, hooks lifecycle.ServiceBindingHooks) *reconcilers.AdmissionWebhookAdapter[*unstructured.Unstructured] {
return &reconcilers.AdmissionWebhookAdapter[*unstructured.Unstructured]{
Name: "AdmissionProjectorWebhook",
Reconciler: &reconcilers.SyncReconciler[*unstructured.Unstructured]{
Expand Down Expand Up @@ -135,13 +134,33 @@ func AdmissionProjectorWebhook(c reconcilers.Config) *reconcilers.AdmissionWebho
}

// project active bindings into workload
projector := projector.New(resolver.New(c))
if f := hooks.WorkloadPreProjection; f != nil {
if err := f(ctx, workload); err != nil {
return err
}
}
projector := hooks.GetProjector(hooks.GetResolver(c))
for i := range activeServiceBindings {
sb := activeServiceBindings[i].DeepCopy()
sb.Default()
if f := hooks.ServiceBindingPreProjection; f != nil {
if err := f(ctx, sb); err != nil {
return err
}
}
if err := projector.Project(ctx, sb, workload); err != nil {
return err
}
if f := hooks.ServiceBindingPostProjection; f != nil {
if err := f(ctx, sb); err != nil {
return err
}
}
}
if f := hooks.WorkloadPostProjection; f != nil {
if err := f(ctx, workload); err != nil {
return err
}
}

return nil
Expand Down
3 changes: 2 additions & 1 deletion controllers/webhook_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import (
servicebindingv1beta1 "github.com/servicebinding/runtime/apis/v1beta1"
"github.com/servicebinding/runtime/controllers"
dieservicebindingv1beta1 "github.com/servicebinding/runtime/dies/v1beta1"
"github.com/servicebinding/runtime/lifecycle"
"github.com/servicebinding/runtime/rbac"
)

Expand Down Expand Up @@ -519,7 +520,7 @@ func TestAdmissionProjectorWebhook(t *testing.T) {
wts.Run(t, scheme, func(t *testing.T, tc *rtesting.AdmissionWebhookTestCase, c reconcilers.Config) *admission.Webhook {
restMapper := c.RESTMapper().(*meta.DefaultRESTMapper)
restMapper.Add(schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, meta.RESTScopeNamespace)
return controllers.AdmissionProjectorWebhook(c).Build()
return controllers.AdmissionProjectorWebhook(c, lifecycle.ServiceBindingHooks{}).Build()
})
}

Expand Down
Loading

0 comments on commit 1e04b18

Please sign in to comment.