From 6577c9180c1ea677372d52cc448cd608e1c89b15 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Thu, 5 Oct 2023 14:17:27 -0400 Subject: [PATCH] Unproject workloads no longer targeted by a ServiceBinding When a previously bound workload is no longer targeted by a ServiceBinding it is now unprojected. Previously, the projection was orphaned as the controller only managed workload matching the reference on the ServiceBinding resource. This could happen for a few different reasons: - the name of the referenced workload was updated on the ServiceBinding - the label selector matching the workload was updated on the ServiceBinding - the labels on the workload were updated to no longer match the selector on the ServiceBinding. In order to find previously bound workloads, we now list all resources matching the workload refs GVK in the namespace. That list is filtered to resources that are currently projected, or match the new criteria. All matching workloads are unprojected, but only resources matching the current ref are re-projected. To avoid a much larger search area, the workloadRef apiVersion and kind fields are now immutable. Users who need to update either of these values will need to delete the ServiceBinding and create a new resource with the desired values. Signed-off-by: Scott Andrews --- apis/v1beta1/servicebinding_test.go | 165 ++++++++++++++++++ apis/v1beta1/servicebinding_webhook.go | 29 ++- controllers/servicebinding_controller.go | 45 ++++- controllers/servicebinding_controller_test.go | 74 ++++++-- controllers/webhook_controller.go | 7 +- lifecycle/hooks_test.go | 10 ++ projector/binding.go | 35 +++- projector/binding_test.go | 165 +++++++++++++++++- projector/interface.go | 4 +- resolver/cluster.go | 69 ++++---- resolver/cluster_test.go | 67 ++++++- resolver/interface.go | 6 +- 12 files changed, 612 insertions(+), 64 deletions(-) diff --git a/apis/v1beta1/servicebinding_test.go b/apis/v1beta1/servicebinding_test.go index e885e608..74a650c1 100644 --- a/apis/v1beta1/servicebinding_test.go +++ b/apis/v1beta1/servicebinding_test.go @@ -20,7 +20,9 @@ import ( "testing" "github.com/google/go-cmp/cmp" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/validation/field" ) @@ -273,3 +275,166 @@ func TestServiceBindingValidate(t *testing.T) { }) } } + +func TestServiceBindingValidate_Immutable(t *testing.T) { + tests := []struct { + name string + seed *ServiceBinding + old runtime.Object + expected field.ErrorList + }{ + { + name: "allow update workload name", + seed: &ServiceBinding{ + Spec: ServiceBindingSpec{ + Name: "my-binding", + Service: ServiceBindingServiceReference{ + APIVersion: "v1", + Kind: "Secret", + Name: "my-service", + }, + Workload: ServiceBindingWorkloadReference{ + APIVersion: "apps/v1", + Kind: "Deloyment", + Name: "new-workload", + }, + }, + }, + old: &ServiceBinding{ + Spec: ServiceBindingSpec{ + Name: "my-binding", + Service: ServiceBindingServiceReference{ + APIVersion: "v1", + Kind: "Secret", + Name: "my-service", + }, + Workload: ServiceBindingWorkloadReference{ + APIVersion: "apps/v1", + Kind: "Deloyment", + Name: "old-workload", + }, + }, + }, + expected: field.ErrorList{}, + }, + { + name: "reject update workload apiVersion", + seed: &ServiceBinding{ + Spec: ServiceBindingSpec{ + Name: "my-binding", + Service: ServiceBindingServiceReference{ + APIVersion: "v1", + Kind: "Secret", + Name: "my-service", + }, + Workload: ServiceBindingWorkloadReference{ + APIVersion: "apps/v1", + Kind: "Deloyment", + Name: "my-workload", + }, + }, + }, + old: &ServiceBinding{ + Spec: ServiceBindingSpec{ + Name: "my-binding", + Service: ServiceBindingServiceReference{ + APIVersion: "v1", + Kind: "Secret", + Name: "my-service", + }, + Workload: ServiceBindingWorkloadReference{ + APIVersion: "extensions/v1beta1", + Kind: "Deloyment", + Name: "my-workload", + }, + }, + }, + expected: field.ErrorList{ + { + Type: field.ErrorTypeForbidden, + Field: "spec.workload.apiVersion", + Detail: "Workload apiVersion is immutable. Delete and recreate the ServiceBinding to update.", + BadValue: "", + }, + }, + }, + { + name: "reject update workload kind", + seed: &ServiceBinding{ + Spec: ServiceBindingSpec{ + Name: "my-binding", + Service: ServiceBindingServiceReference{ + APIVersion: "v1", + Kind: "Secret", + Name: "my-service", + }, + Workload: ServiceBindingWorkloadReference{ + APIVersion: "apps/v1", + Kind: "Deloyment", + Name: "my-workload", + }, + }, + }, + old: &ServiceBinding{ + Spec: ServiceBindingSpec{ + Name: "my-binding", + Service: ServiceBindingServiceReference{ + APIVersion: "v1", + Kind: "Secret", + Name: "my-service", + }, + Workload: ServiceBindingWorkloadReference{ + APIVersion: "apps/v1", + Kind: "StatefulSet", + Name: "my-workload", + }, + }, + }, + expected: field.ErrorList{ + { + Type: field.ErrorTypeForbidden, + Field: "spec.workload.kind", + Detail: "Workload kind is immutable. Delete and recreate the ServiceBinding to update.", + BadValue: "", + }, + }, + }, + { + name: "unkonwn old object", + seed: &ServiceBinding{ + Spec: ServiceBindingSpec{ + Name: "my-binding", + Service: ServiceBindingServiceReference{ + APIVersion: "v1", + Kind: "Secret", + Name: "my-service", + }, + Workload: ServiceBindingWorkloadReference{ + APIVersion: "apps/v1", + Kind: "Deloyment", + Name: "new-workload", + }, + }, + }, + old: &corev1.Pod{}, + expected: field.ErrorList{ + { + Type: field.ErrorTypeInternal, + Field: "", + Detail: "old object must be of type v1beta1.ServiceBinding", + }, + }, + }, + } + + for _, c := range tests { + t.Run(c.name, func(t *testing.T) { + expectedErr := c.expected.ToAggregate() + + _, actualUpdateErr := c.seed.ValidateUpdate(c.old) + if diff := cmp.Diff(expectedErr, actualUpdateErr); diff != "" { + t.Errorf("ValidateCreate (-expected, +actual): %s", diff) + } + }) + } +} diff --git a/apis/v1beta1/servicebinding_webhook.go b/apis/v1beta1/servicebinding_webhook.go index 9c3bb40a..11a76a17 100644 --- a/apis/v1beta1/servicebinding_webhook.go +++ b/apis/v1beta1/servicebinding_webhook.go @@ -17,6 +17,8 @@ limitations under the License. package v1beta1 import ( + "fmt" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/validation/field" @@ -52,9 +54,32 @@ func (r *ServiceBinding) ValidateCreate() (admission.Warnings, error) { // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type func (r *ServiceBinding) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { - // TODO(user): check for immutable fields, if any r.Default() - return nil, r.validate().ToAggregate() + + errs := field.ErrorList{} + + // check immutable fields + if ro, ok := old.(*ServiceBinding); !ok { + errs = append(errs, + field.InternalError(nil, fmt.Errorf("old object must be of type v1beta1.ServiceBinding")), + ) + } else { + if r.Spec.Workload.APIVersion != ro.Spec.Workload.APIVersion { + errs = append(errs, + field.Forbidden(field.NewPath("spec", "workload", "apiVersion"), "Workload apiVersion is immutable. Delete and recreate the ServiceBinding to update."), + ) + } + if r.Spec.Workload.Kind != ro.Spec.Workload.Kind { + errs = append(errs, + field.Forbidden(field.NewPath("spec", "workload", "kind"), "Workload kind is immutable. Delete and recreate the ServiceBinding to update."), + ) + } + } + + // validate new object + errs = append(errs, r.validate()...) + + return nil, errs.ToAggregate() } // ValidateDelete implements webhook.Validator so a webhook will be registered for the type diff --git a/controllers/servicebinding_controller.go b/controllers/servicebinding_controller.go index 35c85189..93057f48 100644 --- a/controllers/servicebinding_controller.go +++ b/controllers/servicebinding_controller.go @@ -22,10 +22,13 @@ import ( "github.com/vmware-labs/reconciler-runtime/apis" "github.com/vmware-labs/reconciler-runtime/reconcilers" + "github.com/vmware-labs/reconciler-runtime/tracker" corev1 "k8s.io/api/core/v1" apierrs "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" ctlr "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/handler" @@ -122,20 +125,32 @@ func ResolveWorkloads(hooks lifecycle.ServiceBindingHooks) reconcilers.SubReconc SyncWithResult: func(ctx context.Context, resource *servicebindingv1beta1.ServiceBinding) (reconcile.Result, error) { c := reconcilers.RetrieveConfigOrDie(ctx) + trackingRef := tracker.Reference{ + APIGroup: schema.FromAPIVersionAndKind(resource.Spec.Workload.APIVersion, "").Group, + Kind: resource.Spec.Workload.Kind, + Namespace: resource.Namespace, + } + if resource.Spec.Workload.Name != "" { + trackingRef.Name = resource.Spec.Workload.Name + } + if resource.Spec.Workload.Selector != nil { + selector, err := metav1.LabelSelectorAsSelector(resource.Spec.Workload.Selector) + if err != nil { + // should never get here + return reconcile.Result{}, err + } + trackingRef.Selector = selector + } + c.Tracker.TrackReference(trackingRef, resource) + ref := corev1.ObjectReference{ APIVersion: resource.Spec.Workload.APIVersion, Kind: resource.Spec.Workload.Kind, Namespace: resource.Namespace, Name: resource.Spec.Workload.Name, } - workloads, err := hooks.GetResolver(TrackingClient(c)).LookupWorkloads(ctx, ref, resource.Spec.Workload.Selector) + workloads, err := hooks.GetResolver(c).LookupWorkloads(ctx, ref, resource.Spec.Workload.Selector, resource.UID) if err != nil { - if apierrs.IsNotFound(err) { - // leave Unknown, the workload may be created shortly - resource.GetConditionManager().MarkUnknown(servicebindingv1beta1.ServiceBindingConditionWorkloadProjected, "WorkloadNotFound", "the workload was not found") - // TODO use track rather than requeue - return reconcile.Result{Requeue: true}, nil - } if apierrs.IsForbidden(err) { // set False, the operator needs to give access to the resource // see https://servicebinding.io/spec/core/1.0.0/#considerations-for-role-based-access-control-rbac-1 @@ -144,12 +159,24 @@ func ResolveWorkloads(hooks lifecycle.ServiceBindingHooks) reconcilers.SubReconc } else { resource.GetConditionManager().MarkFalse(servicebindingv1beta1.ServiceBindingConditionWorkloadProjected, "WorkloadForbidden", "the controller does not have permission to get the workload") } - // TODO use track rather than requeue - return reconcile.Result{Requeue: true}, nil + return reconcile.Result{}, nil } // TODO handle other err cases return reconcile.Result{}, err } + if resource.Spec.Workload.Name != "" { + found := false + for _, workload := range workloads { + if workload.(metav1.Object).GetName() == resource.Spec.Workload.Name { + found = true + break + } + } + if !found { + // leave Unknown, the workload may be created shortly + resource.GetConditionManager().MarkUnknown(servicebindingv1beta1.ServiceBindingConditionWorkloadProjected, "WorkloadNotFound", "the workload was not found") + } + } StashWorkloads(ctx, workloads) diff --git a/controllers/servicebinding_controller_test.go b/controllers/servicebinding_controller_test.go index 954041dc..77046d26 100644 --- a/controllers/servicebinding_controller_test.go +++ b/controllers/servicebinding_controller_test.go @@ -38,6 +38,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/util/uuid" clientgoscheme "k8s.io/client-go/kubernetes/scheme" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -89,13 +90,12 @@ func TestServiceBindingReconciler(t *testing.T) { }) workload := dieappsv1.DeploymentBlank. - DieStamp(func(r *appsv1.Deployment) { - r.APIVersion = "apps/v1" - r.Kind = "Deployment" - }). + APIVersion("apps/v1"). + Kind("Deployment"). MetadataDie(func(d *diemetav1.ObjectMetaDie) { d.Namespace(namespace) d.Name("my-workload") + d.UID(uuid.NewUUID()) }). SpecDie(func(d *dieappsv1.DeploymentSpecDie) { d.TemplateDie(func(d *diecorev1.PodTemplateSpecDie) { @@ -158,6 +158,8 @@ func TestServiceBindingReconciler(t *testing.T) { unstructured.SetNestedSlice(unprojectedWorkload.UnstructuredContent(), containers, "spec", "template", "spec", "containers") unstructured.SetNestedSlice(unprojectedWorkload.UnstructuredContent(), []interface{}{}, "spec", "template", "spec", "volumes") + newWorkloadUID := uuid.NewUUID() + rts := rtesting.ReconcilerTests{ "in sync": { Request: request, @@ -265,6 +267,59 @@ func TestServiceBindingReconciler(t *testing.T) { }), }, }, + "switch bound workload": { + Request: request, + StatusSubResourceTypes: []client.Object{ + &servicebindingv1beta1.ServiceBinding{}, + }, + GivenObjects: []client.Object{ + serviceBinding. + MetadataDie(func(d *diemetav1.ObjectMetaDie) { + d.Finalizers("servicebinding.io/finalizer") + }). + SpecDie(func(d *dieservicebindingv1beta1.ServiceBindingSpecDie) { + d.WorkloadDie(func(d *dieservicebindingv1beta1.ServiceBindingWorkloadReferenceDie) { + d.Name("new-workload") + }) + }). + StatusDie(func(d *dieservicebindingv1beta1.ServiceBindingStatusDie) { + d.ConditionsDie( + dieservicebindingv1beta1.ServiceBindingConditionReady.True().Reason("ServiceBound"), + dieservicebindingv1beta1.ServiceBindingConditionServiceAvailable.True().Reason("ResolvedBindingSecret"), + dieservicebindingv1beta1.ServiceBindingConditionWorkloadProjected.True().Reason("WorkloadProjected"), + ) + d.BindingDie(func(d *dieservicebindingv1beta1.ServiceBindingSecretReferenceDie) { + d.Name(secretName) + }) + }), + projectedWorkload, + workload. + MetadataDie(func(d *diemetav1.ObjectMetaDie) { + d.Name("new-workload") + d.UID(newWorkloadUID) + }), + }, + ExpectTracks: []rtesting.TrackRequest{ + rtesting.NewTrackRequest(workload.MetadataDie(func(d *diemetav1.ObjectMetaDie) { d.Name("new-workload") }), serviceBinding, scheme), + rtesting.NewTrackRequest(workloadMapping, serviceBinding, scheme), + rtesting.NewTrackRequest(workloadMapping, serviceBinding, scheme), + }, + ExpectEvents: []rtesting.Event{ + rtesting.NewEvent(serviceBinding, scheme, corev1.EventTypeNormal, "Updated", "Updated Deployment %q", workload.GetName()), + rtesting.NewEvent(serviceBinding, scheme, corev1.EventTypeNormal, "Updated", "Updated Deployment %q", "new-workload"), + }, + ExpectUpdates: []client.Object{ + // unproject my-workload + unprojectedWorkload, + // project new-workload + projectedWorkload. + MetadataDie(func(d *diemetav1.ObjectMetaDie) { + d.Name("new-workload") + d.UID(newWorkloadUID) + }). + DieReleaseUnstructured(), + }, + }, "terminating": { Request: request, StatusSubResourceTypes: []client.Object{ @@ -604,7 +659,6 @@ func TestResolveWorkload(t *testing.T) { }) }). DieReleasePtr(), - ExpectedResult: reconcile.Result{Requeue: true}, ExpectResource: serviceBinding. SpecDie(func(d *dieservicebindingv1beta1.ServiceBindingSpecDie) { d.WorkloadDie(func(d *dieservicebindingv1beta1.ServiceBindingWorkloadReferenceDie) { @@ -642,11 +696,10 @@ func TestResolveWorkload(t *testing.T) { workload3, }, WithReactors: []rtesting.ReactionFunc{ - rtesting.InduceFailure("get", "Deployment", rtesting.InduceFailureOpts{ - Error: apierrs.NewForbidden(schema.GroupResource{}, "my-workload-1", fmt.Errorf("test forbidden")), + rtesting.InduceFailure("list", "DeploymentList", rtesting.InduceFailureOpts{ + Error: apierrs.NewForbidden(schema.GroupResource{}, "", fmt.Errorf("test forbidden")), }), }, - ExpectedResult: reconcile.Result{Requeue: true}, ExpectResource: serviceBinding. SpecDie(func(d *dieservicebindingv1beta1.ServiceBindingSpecDie) { d.WorkloadDie(func(d *dieservicebindingv1beta1.ServiceBindingWorkloadReferenceDie) { @@ -761,7 +814,6 @@ func TestResolveWorkload(t *testing.T) { Error: apierrs.NewForbidden(schema.GroupResource{}, "", fmt.Errorf("test forbidden")), }), }, - ExpectedResult: reconcile.Result{Requeue: true}, ExpectResource: serviceBinding. SpecDie(func(d *dieservicebindingv1beta1.ServiceBindingSpecDie) { d.WorkloadDie(func(d *dieservicebindingv1beta1.ServiceBindingWorkloadReferenceDie) { @@ -922,7 +974,7 @@ func TestProjectBinding(t *testing.T) { d.WorkloadDie(func(d *dieservicebindingv1beta1.ServiceBindingWorkloadReferenceDie) { d.APIVersion("apps/v1") d.Kind("Deployment") - d.Name("my-workload-1") + d.Name(workload.GetName()) }) }). DieReleasePtr(), @@ -950,7 +1002,7 @@ func TestProjectBinding(t *testing.T) { d.WorkloadDie(func(d *dieservicebindingv1beta1.ServiceBindingWorkloadReferenceDie) { d.APIVersion("apps/v1") d.Kind("Deployment") - d.Name("my-workload-1") + d.Name(workload.GetName()) }) }). DieReleasePtr(), diff --git a/controllers/webhook_controller.go b/controllers/webhook_controller.go index 20aa1fe9..00b125d5 100644 --- a/controllers/webhook_controller.go +++ b/controllers/webhook_controller.go @@ -110,12 +110,18 @@ func AdmissionProjectorWebhook(c reconcilers.Config, hooks lifecycle.ServiceBind return err } + projector := hooks.GetProjector(hooks.GetResolver(c)) + // check that bindings are for this workload activeServiceBindings := []servicebindingv1beta1.ServiceBinding{} for _, sb := range serviceBindings.Items { if !sb.DeletionTimestamp.IsZero() { continue } + if projector.IsProjected(ctx, &sb, workload) { + activeServiceBindings = append(activeServiceBindings, sb) + continue + } ref := sb.Spec.Workload if ref.Name == workload.GetName() { activeServiceBindings = append(activeServiceBindings, sb) @@ -139,7 +145,6 @@ func AdmissionProjectorWebhook(c reconcilers.Config, hooks lifecycle.ServiceBind return err } } - projector := hooks.GetProjector(hooks.GetResolver(c)) for i := range activeServiceBindings { sb := activeServiceBindings[i].DeepCopy() sb.Default() diff --git a/lifecycle/hooks_test.go b/lifecycle/hooks_test.go index 1c122bac..2dfa64fe 100644 --- a/lifecycle/hooks_test.go +++ b/lifecycle/hooks_test.go @@ -29,6 +29,7 @@ import ( rtesting "github.com/vmware-labs/reconciler-runtime/testing" "github.com/vmware-labs/reconciler-runtime/tracker" "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" @@ -291,6 +292,15 @@ func (p *mockProjector) Unproject(ctx context.Context, binding *servicebindingv1 return p.m.MethodCalled("Projector.Unproject", *p.i, ctx, binding, workload).Error(0) } +func (p *mockProjector) IsProjected(ctx context.Context, binding *servicebindingv1beta1.ServiceBinding, workload runtime.Object) bool { + annotations := workload.(metav1.Object).GetAnnotations() + if len(annotations) == 0 { + return false + } + _, ok := annotations[fmt.Sprintf("%s%s", projector.MappingAnnotationPrefix, workload.(metav1.Object).GetUID())] + return ok +} + func makeHooks() (lifecycle.ServiceBindingHooks, *mock.Mock) { m := &mock.Mock{} i := pointer.Int(0) diff --git a/projector/binding.go b/projector/binding.go index 85b124c2..4d12db5f 100644 --- a/projector/binding.go +++ b/projector/binding.go @@ -27,6 +27,7 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/sets" @@ -68,6 +69,10 @@ func (p *serviceBindingProjector) Project(ctx context.Context, binding *serviceb return err } + if !p.shouldProject(binding, workload) { + return nil + } + versionMapping := MappingVersion(version, resourceMapping) mpt, err := NewMetaPodTemplate(ctx, workload, versionMapping) if err != nil { @@ -117,6 +122,15 @@ func (p *serviceBindingProjector) Unproject(ctx context.Context, binding *servic return nil } +func (p *serviceBindingProjector) IsProjected(ctx context.Context, binding *servicebindingv1beta1.ServiceBinding, workload runtime.Object) bool { + annotations := workload.(metav1.Object).GetAnnotations() + if len(annotations) == 0 { + return false + } + _, ok := annotations[fmt.Sprintf("%s%s", MappingAnnotationPrefix, workload.(metav1.Object).GetUID())] + return ok +} + type mappingValue struct { WorkloadMapping *servicebindingv1beta1.ClusterWorkloadResourceMappingSpec RESTMapping *meta.RESTMapping @@ -146,11 +160,28 @@ func (p *serviceBindingProjector) lookupClusterMapping(ctx context.Context, work return ctx, wm, rm.Resource.Version, nil } -func (p *serviceBindingProjector) project(binding *servicebindingv1beta1.ServiceBinding, mpt *metaPodTemplate) { +func (p *serviceBindingProjector) shouldProject(binding *servicebindingv1beta1.ServiceBinding, workload runtime.Object) bool { if p.secretName(binding) == "" { // no secret to bind - return + return false } + + if binding.Spec.Workload.Name != "" { + return binding.Spec.Workload.Name == workload.(metav1.Object).GetName() + } + if binding.Spec.Workload.Selector != nil { + ls, err := metav1.LabelSelectorAsSelector(binding.Spec.Workload.Selector) + if err != nil { + // should never get here + return false + } + return ls.Matches(labels.Set(workload.(metav1.Object).GetLabels())) + } + + return false +} + +func (p *serviceBindingProjector) project(binding *servicebindingv1beta1.ServiceBinding, mpt *metaPodTemplate) { p.projectVolume(binding, mpt) for i := range mpt.Containers { p.projectContainer(binding, mpt, &mpt.Containers[i]) diff --git a/projector/binding_test.go b/projector/binding_test.go index 4a71b51b..291f4efb 100644 --- a/projector/binding_test.go +++ b/projector/binding_test.go @@ -70,6 +70,11 @@ func TestBinding(t *testing.T) { }, Spec: servicebindingv1beta1.ServiceBindingSpec{ Name: bindingName, + Workload: servicebindingv1beta1.ServiceBindingWorkloadReference{ + APIVersion: "apps/v1", + Kind: "Deployment", + Name: "my-workload", + }, }, Status: servicebindingv1beta1.ServiceBindingStatus{ Binding: &servicebindingv1beta1.ServiceBindingSecretReference{ @@ -78,6 +83,9 @@ func TestBinding(t *testing.T) { }, }, workload: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-workload", + }, Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ @@ -109,6 +117,7 @@ func TestBinding(t *testing.T) { }, expected: &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ + Name: "my-workload", Annotations: map[string]string{ "projector.servicebinding.io/mapping-26894874-4719-4802-8f43-8ceed127b4c2": podSpecableMapping, }, @@ -239,6 +248,11 @@ func TestBinding(t *testing.T) { }, Spec: servicebindingv1beta1.ServiceBindingSpec{ Name: bindingName, + Workload: servicebindingv1beta1.ServiceBindingWorkloadReference{ + APIVersion: "batch/v1", + Kind: "CronJob", + Name: "my-workload", + }, }, Status: servicebindingv1beta1.ServiceBindingStatus{ Binding: &servicebindingv1beta1.ServiceBindingSecretReference{ @@ -247,6 +261,9 @@ func TestBinding(t *testing.T) { }, }, workload: &batchv1.CronJob{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-workload", + }, Spec: batchv1.CronJobSpec{ JobTemplate: batchv1.JobTemplateSpec{ Spec: batchv1.JobSpec{ @@ -282,6 +299,7 @@ func TestBinding(t *testing.T) { }, expected: &batchv1.CronJob{ ObjectMeta: metav1.ObjectMeta{ + Name: "my-workload", Annotations: map[string]string{ "projector.servicebinding.io/mapping-26894874-4719-4802-8f43-8ceed127b4c2": cronJobMapping, }, @@ -398,6 +416,11 @@ func TestBinding(t *testing.T) { }, Spec: servicebindingv1beta1.ServiceBindingSpec{ Name: bindingName, + Workload: servicebindingv1beta1.ServiceBindingWorkloadReference{ + APIVersion: "batch/v1", + Kind: "CronJob", + Name: "my-workload", + }, }, Status: servicebindingv1beta1.ServiceBindingStatus{ Binding: nil, @@ -405,6 +428,7 @@ func TestBinding(t *testing.T) { }, workload: &batchv1.CronJob{ ObjectMeta: metav1.ObjectMeta{ + Name: "my-workload", Annotations: map[string]string{ "projector.servicebinding.io/mapping-26894874-4719-4802-8f43-8ceed127b4c2": cronJobMapping, }, @@ -513,12 +537,16 @@ func TestBinding(t *testing.T) { }, expected: &batchv1.CronJob{ ObjectMeta: metav1.ObjectMeta{ + Name: "my-workload", Annotations: map[string]string{}, }, Spec: batchv1.CronJobSpec{ JobTemplate: batchv1.JobTemplateSpec{ Spec: batchv1.JobSpec{ Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{}, + }, Spec: corev1.PodSpec{ InitContainers: []corev1.Container{ { @@ -529,6 +557,7 @@ func TestBinding(t *testing.T) { Value: "/bindings", }, }, + VolumeMounts: []corev1.VolumeMount{}, }, { Name: "init-hello-2", @@ -538,6 +567,7 @@ func TestBinding(t *testing.T) { Value: "/bindings", }, }, + VolumeMounts: []corev1.VolumeMount{}, }, }, Containers: []corev1.Container{ @@ -549,6 +579,7 @@ func TestBinding(t *testing.T) { Value: "/custom/path", }, }, + VolumeMounts: []corev1.VolumeMount{}, }, { Name: "hello-2", @@ -558,8 +589,10 @@ func TestBinding(t *testing.T) { Value: "/bindings", }, }, + VolumeMounts: []corev1.VolumeMount{}, }, }, + Volumes: []corev1.Volume{}, }, }, }, @@ -594,6 +627,11 @@ func TestBinding(t *testing.T) { }, Spec: servicebindingv1beta1.ServiceBindingSpec{ Name: bindingName, + Workload: servicebindingv1beta1.ServiceBindingWorkloadReference{ + APIVersion: "batch/v1", + Kind: "CronJob", + Name: "my-workload", + }, }, Status: servicebindingv1beta1.ServiceBindingStatus{ Binding: nil, @@ -601,6 +639,7 @@ func TestBinding(t *testing.T) { }, workload: &batchv1.CronJob{ ObjectMeta: metav1.ObjectMeta{ + Name: "my-workload", Annotations: map[string]string{}, }, Spec: batchv1.CronJobSpec{ @@ -707,6 +746,7 @@ func TestBinding(t *testing.T) { }, expected: &batchv1.CronJob{ ObjectMeta: metav1.ObjectMeta{ + Name: "my-workload", Annotations: map[string]string{}, }, Spec: batchv1.CronJobSpec{ @@ -778,6 +818,11 @@ func TestBinding(t *testing.T) { }, Spec: servicebindingv1beta1.ServiceBindingSpec{ Name: bindingName, + Workload: servicebindingv1beta1.ServiceBindingWorkloadReference{ + APIVersion: "batch/v1", + Kind: "CronJob", + Name: "my-workload", + }, }, Status: servicebindingv1beta1.ServiceBindingStatus{ Binding: nil, @@ -785,6 +830,7 @@ func TestBinding(t *testing.T) { }, workload: &batchv1.CronJob{ ObjectMeta: metav1.ObjectMeta{ + Name: "my-workload", Annotations: map[string]string{}, }, Spec: batchv1.CronJobSpec{ @@ -891,6 +937,7 @@ func TestBinding(t *testing.T) { }, expected: &batchv1.CronJob{ ObjectMeta: metav1.ObjectMeta{ + Name: "my-workload", Annotations: map[string]string{}, }, Spec: batchv1.CronJobSpec{ @@ -1005,6 +1052,11 @@ func TestBinding(t *testing.T) { }, Spec: servicebindingv1beta1.ServiceBindingSpec{ Name: bindingName, + Workload: servicebindingv1beta1.ServiceBindingWorkloadReference{ + APIVersion: "apps/v1", + Kind: "Deployment", + Name: "my-workload", + }, }, Status: servicebindingv1beta1.ServiceBindingStatus{ Binding: &servicebindingv1beta1.ServiceBindingSecretReference{ @@ -1012,9 +1064,14 @@ func TestBinding(t *testing.T) { }, }, }, - workload: &appsv1.Deployment{}, + workload: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-workload", + }, + }, expected: &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ + Name: "my-workload", Annotations: map[string]string{ "projector.servicebinding.io/mapping-26894874-4719-4802-8f43-8ceed127b4c2": podSpecableMapping, }, @@ -1059,6 +1116,11 @@ func TestBinding(t *testing.T) { }, Spec: servicebindingv1beta1.ServiceBindingSpec{ Name: bindingName, + Workload: servicebindingv1beta1.ServiceBindingWorkloadReference{ + APIVersion: "apps/v1", + Kind: "Deployment", + Name: "my-workload", + }, }, Status: servicebindingv1beta1.ServiceBindingStatus{ Binding: &servicebindingv1beta1.ServiceBindingSecretReference{ @@ -1067,6 +1129,9 @@ func TestBinding(t *testing.T) { }, }, workload: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-workload", + }, Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ @@ -1116,6 +1181,7 @@ func TestBinding(t *testing.T) { }, expected: &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ + Name: "my-workload", Annotations: map[string]string{ "projector.servicebinding.io/mapping-26894874-4719-4802-8f43-8ceed127b4c2": podSpecableMapping, }, @@ -1187,6 +1253,11 @@ func TestBinding(t *testing.T) { Key: "bar", }, }, + Workload: servicebindingv1beta1.ServiceBindingWorkloadReference{ + APIVersion: "apps/v1", + Kind: "Deployment", + Name: "my-workload", + }, }, Status: servicebindingv1beta1.ServiceBindingStatus{ Binding: &servicebindingv1beta1.ServiceBindingSecretReference{ @@ -1195,6 +1266,9 @@ func TestBinding(t *testing.T) { }, }, workload: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-workload", + }, Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ @@ -1207,6 +1281,7 @@ func TestBinding(t *testing.T) { }, expected: &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ + Name: "my-workload", Annotations: map[string]string{ "projector.servicebinding.io/mapping-26894874-4719-4802-8f43-8ceed127b4c2": podSpecableMapping, }, @@ -1290,6 +1365,11 @@ func TestBinding(t *testing.T) { }, Spec: servicebindingv1beta1.ServiceBindingSpec{ Name: bindingName, + Workload: servicebindingv1beta1.ServiceBindingWorkloadReference{ + APIVersion: "apps/v1", + Kind: "Deployment", + Name: "my-workload", + }, }, Status: servicebindingv1beta1.ServiceBindingStatus{ Binding: &servicebindingv1beta1.ServiceBindingSecretReference{ @@ -1298,6 +1378,9 @@ func TestBinding(t *testing.T) { }, }, workload: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-workload", + }, Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ @@ -1369,6 +1452,7 @@ func TestBinding(t *testing.T) { }, expected: &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ + Name: "my-workload", Annotations: map[string]string{ "projector.servicebinding.io/mapping-26894874-4719-4802-8f43-8ceed127b4c2": podSpecableMapping, }, @@ -1440,6 +1524,11 @@ func TestBinding(t *testing.T) { Key: "bloop", }, }, + Workload: servicebindingv1beta1.ServiceBindingWorkloadReference{ + APIVersion: "apps/v1", + Kind: "Deployment", + Name: "my-workload", + }, }, Status: servicebindingv1beta1.ServiceBindingStatus{ Binding: &servicebindingv1beta1.ServiceBindingSecretReference{ @@ -1448,6 +1537,9 @@ func TestBinding(t *testing.T) { }, }, workload: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-workload", + }, Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ @@ -1519,6 +1611,7 @@ func TestBinding(t *testing.T) { }, expected: &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ + Name: "my-workload", Annotations: map[string]string{ "projector.servicebinding.io/mapping-26894874-4719-4802-8f43-8ceed127b4c2": podSpecableMapping, }, @@ -1614,6 +1707,11 @@ func TestBinding(t *testing.T) { Key: "provider", }, }, + Workload: servicebindingv1beta1.ServiceBindingWorkloadReference{ + APIVersion: "apps/v1", + Kind: "Deployment", + Name: "my-workload", + }, }, Status: servicebindingv1beta1.ServiceBindingStatus{ Binding: &servicebindingv1beta1.ServiceBindingSecretReference{ @@ -1622,6 +1720,9 @@ func TestBinding(t *testing.T) { }, }, workload: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-workload", + }, Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ @@ -1634,6 +1735,7 @@ func TestBinding(t *testing.T) { }, expected: &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ + Name: "my-workload", Annotations: map[string]string{ "projector.servicebinding.io/mapping-26894874-4719-4802-8f43-8ceed127b4c2": podSpecableMapping, }, @@ -1747,6 +1849,11 @@ func TestBinding(t *testing.T) { Key: "provider", }, }, + Workload: servicebindingv1beta1.ServiceBindingWorkloadReference{ + APIVersion: "apps/v1", + Kind: "Deployment", + Name: "my-workload", + }, }, Status: servicebindingv1beta1.ServiceBindingStatus{ Binding: &servicebindingv1beta1.ServiceBindingSecretReference{ @@ -1755,6 +1862,9 @@ func TestBinding(t *testing.T) { }, }, workload: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-workload", + }, Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ @@ -1846,6 +1956,7 @@ func TestBinding(t *testing.T) { }, expected: &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ + Name: "my-workload", Annotations: map[string]string{ "projector.servicebinding.io/mapping-26894874-4719-4802-8f43-8ceed127b4c2": podSpecableMapping, }, @@ -1929,9 +2040,17 @@ func TestBinding(t *testing.T) { }, Spec: servicebindingv1beta1.ServiceBindingSpec{ Name: bindingName, + Workload: servicebindingv1beta1.ServiceBindingWorkloadReference{ + APIVersion: "apps/v1", + Kind: "Deployment", + Name: "my-workload", + }, }, }, workload: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-workload", + }, Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ @@ -1944,6 +2063,7 @@ func TestBinding(t *testing.T) { }, expected: &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ + Name: "my-workload", Annotations: map[string]string{}, }, Spec: appsv1.DeploymentSpec{ @@ -1974,6 +2094,9 @@ func TestBinding(t *testing.T) { Spec: servicebindingv1beta1.ServiceBindingSpec{ Name: bindingName, Workload: servicebindingv1beta1.ServiceBindingWorkloadReference{ + APIVersion: "apps/v1", + Kind: "Deployment", + Name: "my-workload", Containers: []string{"bind"}, }, }, @@ -1984,6 +2107,9 @@ func TestBinding(t *testing.T) { }, }, workload: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-workload", + }, Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ @@ -2004,6 +2130,7 @@ func TestBinding(t *testing.T) { }, expected: &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ + Name: "my-workload", Annotations: map[string]string{ "projector.servicebinding.io/mapping-26894874-4719-4802-8f43-8ceed127b4c2": podSpecableMapping, }, @@ -2082,6 +2209,11 @@ func TestBinding(t *testing.T) { Key: "foo", }, }, + Workload: servicebindingv1beta1.ServiceBindingWorkloadReference{ + APIVersion: "apps/v1", + Kind: "Deployment", + Name: "my-workload", + }, }, Status: servicebindingv1beta1.ServiceBindingStatus{ Binding: &servicebindingv1beta1.ServiceBindingSecretReference{ @@ -2090,6 +2222,9 @@ func TestBinding(t *testing.T) { }, }, workload: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-workload", + }, Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ @@ -2251,6 +2386,7 @@ func TestBinding(t *testing.T) { }, expected: &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ + Name: "my-workload", Annotations: map[string]string{ "projector.servicebinding.io/mapping-26894874-4719-4802-8f43-8ceed127b4c2": podSpecableMapping, }, @@ -2456,6 +2592,13 @@ func TestBinding(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ UID: uid, }, + Spec: servicebindingv1beta1.ServiceBindingSpec{ + Workload: servicebindingv1beta1.ServiceBindingWorkloadReference{ + APIVersion: "apps/v1", + Kind: "Deployment", + Name: "my-workload", + }, + }, Status: servicebindingv1beta1.ServiceBindingStatus{ Binding: &servicebindingv1beta1.ServiceBindingSecretReference{ Name: secretName, @@ -2463,6 +2606,9 @@ func TestBinding(t *testing.T) { }, }, workload: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-workload", + }, Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ @@ -2558,6 +2704,7 @@ func TestBinding(t *testing.T) { }, expected: &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ + Name: "my-workload", Annotations: map[string]string{ "projector.servicebinding.io/mapping-26894874-4719-4802-8f43-8ceed127b4c2": podSpecableMapping, }, @@ -2670,8 +2817,20 @@ func TestBinding(t *testing.T) { }, }, }, deploymentRESTMapping), - binding: &servicebindingv1beta1.ServiceBinding{}, - workload: &appsv1.Deployment{}, + binding: &servicebindingv1beta1.ServiceBinding{ + Spec: servicebindingv1beta1.ServiceBindingSpec{ + Workload: servicebindingv1beta1.ServiceBindingWorkloadReference{ + APIVersion: "apps/v1", + Kind: "Deployment", + Name: "my-workload", + }, + }, + }, + workload: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-workload", + }, + }, expectedErr: true, }, { diff --git a/projector/interface.go b/projector/interface.go index 9b50159b..2d249ceb 100644 --- a/projector/interface.go +++ b/projector/interface.go @@ -29,8 +29,10 @@ import ( type ServiceBindingProjector interface { // Project the service into the workload as defined by the ServiceBinding. Project(ctx context.Context, binding *servicebindingv1beta1.ServiceBinding, workload runtime.Object) error - // Unproject the serice from the workload as defined by the ServiceBinding. + // Unproject the service from the workload as defined by the ServiceBinding. Unproject(ctx context.Context, binding *servicebindingv1beta1.ServiceBinding, workload runtime.Object) error + // IsProjected returns true when the workload has been projected into by the binding + IsProjected(ctx context.Context, binding *servicebindingv1beta1.ServiceBinding, workload runtime.Object) bool } type MappingSource interface { diff --git a/resolver/cluster.go b/resolver/cluster.go index a554172b..915c8c2a 100644 --- a/resolver/cluster.go +++ b/resolver/cluster.go @@ -19,12 +19,14 @@ package resolver import ( "context" "fmt" + "sort" corev1 "k8s.io/api/core/v1" apierrs "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" @@ -97,43 +99,50 @@ func (r *clusterResolver) LookupBindingSecret(ctx context.Context, serviceRef co return secretName, err } -func (r *clusterResolver) LookupWorkloads(ctx context.Context, workloadRef corev1.ObjectReference, selector *metav1.LabelSelector) ([]runtime.Object, error) { - if workloadRef.Name != "" { - workload, err := r.lookupWorkload(ctx, workloadRef) +const ( + mappingAnnotationPrefix = "projector.servicebinding.io/mapping-" +) + +func (r *clusterResolver) LookupWorkloads(ctx context.Context, workloadRef corev1.ObjectReference, selector *metav1.LabelSelector, bindingUID types.UID) ([]runtime.Object, error) { + list := &unstructured.UnstructuredList{} + list.SetAPIVersion(workloadRef.APIVersion) + list.SetKind(fmt.Sprintf("%sList", workloadRef.Kind)) + + var ls labels.Selector + if selector != nil { + var err error + ls, err = metav1.LabelSelectorAsSelector(selector) if err != nil { return nil, err } - return []runtime.Object{workload}, nil } - return r.lookupWorkloads(ctx, workloadRef, selector) -} -func (r *clusterResolver) lookupWorkload(ctx context.Context, workloadRef corev1.ObjectReference) (runtime.Object, error) { - workload := &unstructured.Unstructured{} - workload.SetAPIVersion(workloadRef.APIVersion) - workload.SetKind(workloadRef.Kind) - if err := r.client.Get(ctx, client.ObjectKey{Namespace: workloadRef.Namespace, Name: workloadRef.Name}, workload); err != nil { + if err := r.client.List(ctx, list, client.InNamespace(workloadRef.Namespace)); err != nil { return nil, err } - return workload, nil -} - -func (r *clusterResolver) lookupWorkloads(ctx context.Context, workloadRef corev1.ObjectReference, selector *metav1.LabelSelector) ([]runtime.Object, error) { - workloads := &unstructured.UnstructuredList{} - workloads.SetAPIVersion(workloadRef.APIVersion) - workloads.SetKind(fmt.Sprintf("%sList", workloadRef.Kind)) - ls, err := metav1.LabelSelectorAsSelector(selector) - if err != nil { - return nil, err - } - if err := r.client.List(ctx, workloads, client.InNamespace(workloadRef.Namespace), client.MatchingLabelsSelector{Selector: ls}); err != nil { - return nil, err + workloads := []runtime.Object{} + for i := range list.Items { + workload := &list.Items[i] + if annotations := workload.GetAnnotations(); annotations != nil { + if _, ok := annotations[fmt.Sprintf("%s%s", mappingAnnotationPrefix, bindingUID)]; ok { + workloads = append(workloads, workload) + continue + } + } + if workloadRef.Name != "" { + if workload.GetName() == workloadRef.Name { + workloads = append(workloads, workload) + } + continue + } + if ls.Matches(labels.Set(workload.GetLabels())) { + workloads = append(workloads, workload) + } } - // coerce to []runtime.Object - result := make([]runtime.Object, len(workloads.Items)) - for i := range workloads.Items { - result[i] = &workloads.Items[i] - } - return result, nil + sort.Slice(workloads, func(i, j int) bool { + return workloads[i].(metav1.Object).GetName() < workloads[j].(metav1.Object).GetName() + }) + + return workloads, nil } diff --git a/resolver/cluster_test.go b/resolver/cluster_test.go index bdb79d62..3ec40716 100644 --- a/resolver/cluster_test.go +++ b/resolver/cluster_test.go @@ -18,6 +18,7 @@ package resolver_test import ( "context" + "fmt" "testing" "github.com/google/go-cmp/cmp" @@ -31,12 +32,15 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/util/uuid" clientgoscheme "k8s.io/client-go/kubernetes/scheme" "sigs.k8s.io/controller-runtime/pkg/client" fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" servicebindingv1beta1 "github.com/servicebinding/runtime/apis/v1beta1" + "github.com/servicebinding/runtime/projector" "github.com/servicebinding/runtime/resolver" ) @@ -440,16 +444,19 @@ func TestClusterResolver_LookupWorkloads(t *testing.T) { scheme := runtime.NewScheme() utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + bindingUID := uuid.NewUUID() + tests := []struct { name string givenObjects []client.Object serviceRef corev1.ObjectReference selector *metav1.LabelSelector + bindingUID types.UID expected []runtime.Object expectedErr bool }{ { - name: "not found error", + name: "not found", givenObjects: []client.Object{}, serviceRef: corev1.ObjectReference{ APIVersion: "apps/v1", @@ -457,7 +464,57 @@ func TestClusterResolver_LookupWorkloads(t *testing.T) { Namespace: "my-namespace", Name: "my-workload", }, - expectedErr: true, + bindingUID: bindingUID, + expected: []runtime.Object{}, + }, + { + name: "found previously bound workload", + givenObjects: []client.Object{ + &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "my-namespace", + Name: "previous-workload", + Annotations: map[string]string{ + fmt.Sprintf("%s%s", projector.MappingAnnotationPrefix, bindingUID): "{}", + }, + }, + }, + }, + serviceRef: corev1.ObjectReference{ + APIVersion: "apps/v1", + Kind: "Deployment", + Namespace: "my-namespace", + Name: "my-workload", + }, + bindingUID: bindingUID, + expected: []runtime.Object{ + &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "previous-workload", + "namespace": "my-namespace", + "annotations": map[string]interface{}{ + fmt.Sprintf("%s%s", projector.MappingAnnotationPrefix, bindingUID): "{}", + }, + }, + "spec": map[string]interface{}{ + "selector": nil, + "strategy": map[string]interface{}{}, + "template": map[string]interface{}{ + "metadata": map[string]interface{}{ + "creationTimestamp": nil, + }, + "spec": map[string]interface{}{ + "containers": nil, + }, + }, + }, + "status": map[string]interface{}{}, + }, + }, + }, }, { name: "found workload from scheme", @@ -475,6 +532,7 @@ func TestClusterResolver_LookupWorkloads(t *testing.T) { Namespace: "my-namespace", Name: "my-workload", }, + bindingUID: bindingUID, expected: []runtime.Object{ &unstructured.Unstructured{ Object: map[string]interface{}{ @@ -521,6 +579,7 @@ func TestClusterResolver_LookupWorkloads(t *testing.T) { Namespace: "my-namespace", Name: "my-workload", }, + bindingUID: bindingUID, expected: []runtime.Object{ &unstructured.Unstructured{ Object: map[string]interface{}{ @@ -575,6 +634,7 @@ func TestClusterResolver_LookupWorkloads(t *testing.T) { "app": "my", }, }, + bindingUID: bindingUID, expected: []runtime.Object{ &unstructured.Unstructured{ Object: map[string]interface{}{ @@ -683,6 +743,7 @@ func TestClusterResolver_LookupWorkloads(t *testing.T) { "app": "my", }, }, + bindingUID: bindingUID, expected: []runtime.Object{ &unstructured.Unstructured{ Object: map[string]interface{}{ @@ -724,7 +785,7 @@ func TestClusterResolver_LookupWorkloads(t *testing.T) { Build() resolver := resolver.New(client) - actual, err := resolver.LookupWorkloads(ctx, c.serviceRef, c.selector) + actual, err := resolver.LookupWorkloads(ctx, c.serviceRef, c.selector, c.bindingUID) if (err != nil) != c.expectedErr { t.Errorf("LookupWorkloads() expected err: %v", err) diff --git a/resolver/interface.go b/resolver/interface.go index f617f330..e60da1ab 100644 --- a/resolver/interface.go +++ b/resolver/interface.go @@ -24,6 +24,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" servicebindingv1beta1 "github.com/servicebinding/runtime/apis/v1beta1" ) @@ -43,6 +44,7 @@ type Resolver interface { LookupBindingSecret(ctx context.Context, serviceRef corev1.ObjectReference) (string, error) // LookupWorkloads returns the referenced objects. Often a unstructured Object is used to sidestep issues with schemes and registered - // types. The selector is mutually exclusive with the reference name. - LookupWorkloads(ctx context.Context, workloadRef corev1.ObjectReference, selector *metav1.LabelSelector) ([]runtime.Object, error) + // types. The selector is mutually exclusive with the reference name. The UID of the ServiceBinding is used to find resources that + // may have been previously bound but no longer match the query. + LookupWorkloads(ctx context.Context, workloadRef corev1.ObjectReference, selector *metav1.LabelSelector, uid types.UID) ([]runtime.Object, error) }