diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7039f65a..b637db07 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -342,7 +342,7 @@ jobs: uses: actions/checkout@v4 with: repository: servicebinding/conformance.git - ref: v0.3.1 + ref: v0.3.2 fetch-depth: 1 path: conformance-tests 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..e67d0289 100644 --- a/apis/v1beta1/servicebinding_webhook.go +++ b/apis/v1beta1/servicebinding_webhook.go @@ -17,10 +17,13 @@ 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" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/conversion" "sigs.k8s.io/controller-runtime/pkg/webhook" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" ) @@ -52,9 +55,41 @@ 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 + var ro *ServiceBinding + if o, ok := old.(*ServiceBinding); ok { + ro = o + } else if o, ok := old.(conversion.Convertible); ok { + ro = &ServiceBinding{} + if err := o.ConvertTo(ro); err != nil { + return nil, err + } + } else { + errs = append(errs, + field.InternalError(nil, fmt.Errorf("old object must be of type v1beta1.ServiceBinding")), + ) + } + if len(errs) == 0 { + 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..c5f1757b 100644 --- a/controllers/servicebinding_controller.go +++ b/controllers/servicebinding_controller.go @@ -22,10 +22,12 @@ import ( "github.com/vmware-labs/reconciler-runtime/apis" "github.com/vmware-labs/reconciler-runtime/reconcilers" - corev1 "k8s.io/api/core/v1" + "github.com/vmware-labs/reconciler-runtime/tracker" 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" @@ -63,13 +65,7 @@ func ResolveBindingSecret(hooks lifecycle.ServiceBindingHooks) reconcilers.SubRe Sync: func(ctx context.Context, resource *servicebindingv1beta1.ServiceBinding) error { c := reconcilers.RetrieveConfigOrDie(ctx) - ref := corev1.ObjectReference{ - APIVersion: resource.Spec.Service.APIVersion, - Kind: resource.Spec.Service.Kind, - Namespace: resource.Namespace, - Name: resource.Spec.Service.Name, - } - secretName, err := hooks.GetResolver(TrackingClient(c)).LookupBindingSecret(ctx, ref) + secretName, err := hooks.GetResolver(TrackingClient(c)).LookupBindingSecret(ctx, resource) if err != nil { if apierrs.IsNotFound(err) { // leave Unknown, the provisioned service may be created shortly @@ -122,20 +118,26 @@ func ResolveWorkloads(hooks lifecycle.ServiceBindingHooks) reconcilers.SubReconc SyncWithResult: func(ctx context.Context, resource *servicebindingv1beta1.ServiceBinding) (reconcile.Result, error) { c := reconcilers.RetrieveConfigOrDie(ctx) - ref := corev1.ObjectReference{ - APIVersion: resource.Spec.Workload.APIVersion, - Kind: resource.Spec.Workload.Kind, - Namespace: resource.Namespace, - Name: resource.Spec.Workload.Name, + trackingRef := tracker.Reference{ + APIGroup: schema.FromAPIVersionAndKind(resource.Spec.Workload.APIVersion, "").Group, + Kind: resource.Spec.Workload.Kind, + Namespace: resource.Namespace, } - 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 - resource.GetConditionManager().MarkUnknown(servicebindingv1beta1.ServiceBindingConditionWorkloadProjected, "WorkloadNotFound", "the workload was not found") - // TODO use track rather than requeue - return reconcile.Result{Requeue: true}, nil + 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) + + workloads, err := hooks.GetResolver(c).LookupWorkloads(ctx, resource) + if err != 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 +146,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/go.mod b/go.mod index a0e7eff8..736d99c4 100644 --- a/go.mod +++ b/go.mod @@ -5,9 +5,9 @@ go 1.20 require ( dies.dev v0.9.0 github.com/go-logr/logr v1.2.4 - github.com/google/go-cmp v0.5.9 + github.com/google/go-cmp v0.6.0 github.com/stretchr/testify v1.8.4 - github.com/vmware-labs/reconciler-runtime v0.15.0 + github.com/vmware-labs/reconciler-runtime v0.15.1 gomodules.xyz/jsonpatch/v2 v2.4.0 k8s.io/api v0.28.2 k8s.io/apimachinery v0.28.2 @@ -57,10 +57,10 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.25.0 // indirect golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect - golang.org/x/net v0.15.0 // indirect + golang.org/x/net v0.17.0 // indirect golang.org/x/oauth2 v0.8.0 // indirect - golang.org/x/sys v0.12.0 // indirect - golang.org/x/term v0.12.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/term v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect golang.org/x/time v0.3.0 // indirect gomodules.xyz/jsonpatch/v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index ca99cd5c..c9cbf2a8 100644 --- a/go.sum +++ b/go.sum @@ -45,8 +45,8 @@ github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -114,8 +114,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/vmware-labs/reconciler-runtime v0.15.0 h1:bALsVuM8rIRozcs+gnpVvf9imUSw36q4tX72jHpQE0s= -github.com/vmware-labs/reconciler-runtime v0.15.0/go.mod h1:y0JbNaM0BHlH6yBkIyGmOs/VsIZbKbTFzR178zoOowE= +github.com/vmware-labs/reconciler-runtime v0.15.1 h1:c9lMawF/EkFTWk9QbA7PFb5NAAm99FMDXoJaVkamzw8= +github.com/vmware-labs/reconciler-runtime v0.15.1/go.mod h1:w1X3EW0G6Z6+sTI8VKjKIIDPhDFrO2hFDpiRFZhdNXc= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= @@ -144,8 +144,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -161,11 +161,11 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 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..4e2246e5 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, binding.UID)] + 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..efb49418 100644 --- a/resolver/cluster.go +++ b/resolver/cluster.go @@ -20,11 +20,11 @@ import ( "context" "fmt" - 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" @@ -80,7 +80,8 @@ func (m *clusterResolver) LookupWorkloadMapping(ctx context.Context, gvr schema. return &wrm.Spec, nil } -func (r *clusterResolver) LookupBindingSecret(ctx context.Context, serviceRef corev1.ObjectReference) (string, error) { +func (r *clusterResolver) LookupBindingSecret(ctx context.Context, serviceBinding *servicebindingv1beta1.ServiceBinding) (string, error) { + serviceRef := serviceBinding.Spec.Service if serviceRef.APIVersion == "v1" && serviceRef.Kind == "Secret" { // direct secret reference return serviceRef.Name, nil @@ -88,7 +89,7 @@ func (r *clusterResolver) LookupBindingSecret(ctx context.Context, serviceRef co service := &unstructured.Unstructured{} service.SetAPIVersion(serviceRef.APIVersion) service.SetKind(serviceRef.Kind) - if err := r.client.Get(ctx, client.ObjectKey{Namespace: serviceRef.Namespace, Name: serviceRef.Name}, service); err != nil { + if err := r.client.Get(ctx, client.ObjectKey{Namespace: serviceBinding.Namespace, Name: serviceRef.Name}, service); err != nil { return "", err } secretName, exists, err := unstructured.NestedString(service.UnstructuredContent(), "status", "binding", "name") @@ -97,43 +98,49 @@ 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, serviceBinding *servicebindingv1beta1.ServiceBinding) ([]runtime.Object, error) { + workloadRef := serviceBinding.Spec.Workload + + list := &unstructured.UnstructuredList{} + list.SetAPIVersion(workloadRef.APIVersion) + // TODO this is unsafe if the ListKind doesn't follow this convention + list.SetKind(fmt.Sprintf("%sList", workloadRef.Kind)) + + var ls labels.Selector + if workloadRef.Selector != nil { + var err error + ls, err = metav1.LabelSelectorAsSelector(workloadRef.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(serviceBinding.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, serviceBinding.UID)]; 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 + return workloads, nil } diff --git a/resolver/cluster_test.go b/resolver/cluster_test.go index bdb79d62..ce36438d 100644 --- a/resolver/cluster_test.go +++ b/resolver/cluster_test.go @@ -18,13 +18,13 @@ package resolver_test import ( "context" + "fmt" "testing" "github.com/google/go-cmp/cmp" rtesting "github.com/vmware-labs/reconciler-runtime/testing" appsv1 "k8s.io/api/apps/v1" batchv1 "k8s.io/api/batch/v1" - corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -32,11 +32,13 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" 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" ) @@ -331,20 +333,26 @@ func TestClusterResolver_LookupBindingSecret(t *testing.T) { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) tests := []struct { - name string - givenObjects []client.Object - serviceRef corev1.ObjectReference - expected string - expectedErr bool + name string + givenObjects []client.Object + serviceBinding *servicebindingv1beta1.ServiceBinding + expected string + expectedErr bool }{ { name: "direct binding", givenObjects: []client.Object{}, - serviceRef: corev1.ObjectReference{ - APIVersion: "v1", - Kind: "Secret", - Namespace: "my-namespace", - Name: "my-secret", + serviceBinding: &servicebindingv1beta1.ServiceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "my-namespace", + }, + Spec: servicebindingv1beta1.ServiceBindingSpec{ + Service: servicebindingv1beta1.ServiceBindingServiceReference{ + APIVersion: "v1", + Kind: "Secret", + Name: "my-secret", + }, + }, }, expected: "my-secret", }, @@ -367,11 +375,17 @@ func TestClusterResolver_LookupBindingSecret(t *testing.T) { }, }, }, - serviceRef: corev1.ObjectReference{ - APIVersion: "service.local/v1", - Kind: "ProvisionedService", - Namespace: "my-namespace", - Name: "my-service", + serviceBinding: &servicebindingv1beta1.ServiceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "my-namespace", + }, + Spec: servicebindingv1beta1.ServiceBindingSpec{ + Service: servicebindingv1beta1.ServiceBindingServiceReference{ + APIVersion: "service.local/v1", + Kind: "ProvisionedService", + Name: "my-service", + }, + }, }, expected: "my-secret", }, @@ -390,22 +404,34 @@ func TestClusterResolver_LookupBindingSecret(t *testing.T) { }, }, }, - serviceRef: corev1.ObjectReference{ - APIVersion: "service.local/v1", - Kind: "NotAProvisionedService", - Namespace: "my-namespace", - Name: "my-service", + serviceBinding: &servicebindingv1beta1.ServiceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "my-namespace", + }, + Spec: servicebindingv1beta1.ServiceBindingSpec{ + Service: servicebindingv1beta1.ServiceBindingServiceReference{ + APIVersion: "service.local/v1", + Kind: "NotAProvisionedService", + Name: "my-service", + }, + }, }, expected: "", }, { name: "not found", givenObjects: []client.Object{}, - serviceRef: corev1.ObjectReference{ - APIVersion: "service.local/v1", - Kind: "ProvisionedService", - Namespace: "my-namespace", - Name: "my-service", + serviceBinding: &servicebindingv1beta1.ServiceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "my-namespace", + }, + Spec: servicebindingv1beta1.ServiceBindingSpec{ + Service: servicebindingv1beta1.ServiceBindingServiceReference{ + APIVersion: "service.local/v1", + Kind: "ProvisionedService", + Name: "my-service", + }, + }, }, expectedErr: true, }, @@ -421,7 +447,7 @@ func TestClusterResolver_LookupBindingSecret(t *testing.T) { Build() resolver := resolver.New(client) - actual, err := resolver.LookupBindingSecret(ctx, c.serviceRef) + actual, err := resolver.LookupBindingSecret(ctx, c.serviceBinding) if (err != nil) != c.expectedErr { t.Errorf("LookupBindingSecret() expected err: %v", err) @@ -440,24 +466,87 @@ 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 - expected []runtime.Object - expectedErr bool + name string + givenObjects []client.Object + serviceBinding *servicebindingv1beta1.ServiceBinding + expected []runtime.Object + expectedErr bool }{ { - name: "not found error", + name: "not found", givenObjects: []client.Object{}, - serviceRef: corev1.ObjectReference{ - APIVersion: "apps/v1", - Kind: "Deployment", - Namespace: "my-namespace", - Name: "my-workload", + serviceBinding: &servicebindingv1beta1.ServiceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "my-namespace", + UID: bindingUID, + }, + Spec: servicebindingv1beta1.ServiceBindingSpec{ + Workload: servicebindingv1beta1.ServiceBindingWorkloadReference{ + APIVersion: "apps/v1", + Kind: "Deployment", + Name: "my-workload", + }, + }, + }, + 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): "{}", + }, + }, + }, + }, + serviceBinding: &servicebindingv1beta1.ServiceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "my-namespace", + UID: bindingUID, + }, + Spec: servicebindingv1beta1.ServiceBindingSpec{ + Workload: servicebindingv1beta1.ServiceBindingWorkloadReference{ + APIVersion: "apps/v1", + Kind: "Deployment", + Name: "my-workload", + }, + }, + }, + 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{}{}, + }, + }, }, - expectedErr: true, }, { name: "found workload from scheme", @@ -469,11 +558,18 @@ func TestClusterResolver_LookupWorkloads(t *testing.T) { }, }, }, - serviceRef: corev1.ObjectReference{ - APIVersion: "apps/v1", - Kind: "Deployment", - Namespace: "my-namespace", - Name: "my-workload", + serviceBinding: &servicebindingv1beta1.ServiceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "my-namespace", + UID: bindingUID, + }, + Spec: servicebindingv1beta1.ServiceBindingSpec{ + Workload: servicebindingv1beta1.ServiceBindingWorkloadReference{ + APIVersion: "apps/v1", + Kind: "Deployment", + Name: "my-workload", + }, + }, }, expected: []runtime.Object{ &unstructured.Unstructured{ @@ -515,11 +611,18 @@ func TestClusterResolver_LookupWorkloads(t *testing.T) { }, }, }, - serviceRef: corev1.ObjectReference{ - APIVersion: "workload.local/v1", - Kind: "MyWorkload", - Namespace: "my-namespace", - Name: "my-workload", + serviceBinding: &servicebindingv1beta1.ServiceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "my-namespace", + UID: bindingUID, + }, + Spec: servicebindingv1beta1.ServiceBindingSpec{ + Workload: servicebindingv1beta1.ServiceBindingWorkloadReference{ + APIVersion: "workload.local/v1", + Kind: "MyWorkload", + Name: "my-workload", + }, + }, }, expected: []runtime.Object{ &unstructured.Unstructured{ @@ -565,14 +668,21 @@ func TestClusterResolver_LookupWorkloads(t *testing.T) { }, }, }, - serviceRef: corev1.ObjectReference{ - APIVersion: "apps/v1", - Kind: "Deployment", - Namespace: "my-namespace", - }, - selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "app": "my", + serviceBinding: &servicebindingv1beta1.ServiceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "my-namespace", + UID: bindingUID, + }, + Spec: servicebindingv1beta1.ServiceBindingSpec{ + Workload: servicebindingv1beta1.ServiceBindingWorkloadReference{ + APIVersion: "apps/v1", + Kind: "Deployment", + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "my", + }, + }, + }, }, }, expected: []runtime.Object{ @@ -673,14 +783,21 @@ func TestClusterResolver_LookupWorkloads(t *testing.T) { }, }, }, - serviceRef: corev1.ObjectReference{ - APIVersion: "workload.local/v1", - Kind: "MyWorkload", - Namespace: "my-namespace", - }, - selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "app": "my", + serviceBinding: &servicebindingv1beta1.ServiceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "my-namespace", + UID: bindingUID, + }, + Spec: servicebindingv1beta1.ServiceBindingSpec{ + Workload: servicebindingv1beta1.ServiceBindingWorkloadReference{ + APIVersion: "workload.local/v1", + Kind: "MyWorkload", + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "my", + }, + }, + }, }, }, expected: []runtime.Object{ @@ -724,7 +841,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.serviceBinding) if (err != nil) != c.expectedErr { t.Errorf("LookupWorkloads() expected err: %v", err) diff --git a/resolver/interface.go b/resolver/interface.go index f617f330..2dc1294c 100644 --- a/resolver/interface.go +++ b/resolver/interface.go @@ -19,9 +19,7 @@ package resolver import ( "context" - corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -40,9 +38,10 @@ type Resolver interface { // LookupBindingSecret returns the binding secret name exposed by the service following the Provisioned Service duck-type // (`.status.binding.name`). If a direction binding is used (where the referenced service is itself a Secret) the referenced Secret is // returned without a lookup. - LookupBindingSecret(ctx context.Context, serviceRef corev1.ObjectReference) (string, error) + LookupBindingSecret(ctx context.Context, serviceBinding *servicebindingv1beta1.ServiceBinding) (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, serviceBinding *servicebindingv1beta1.ServiceBinding) ([]runtime.Object, error) }