From 615186e66d49735e4b489ba273fd2a1fc3cfc7b0 Mon Sep 17 00:00:00 2001 From: Scott Andrews Date: Fri, 12 May 2023 15:03:53 -0400 Subject: [PATCH] Track resources with a label selector (#367) Update tracker package to the latest from knative.dev/pkg/tracker, and then heavily modified to remove dependence on Knative packages and align with controller-runtime. The Tracker can now track resources by name or by selector. The Everything selector along with an empty namespace can be used to implicitly track all resource of a given type. The Tracker#TrackReference method can specify either a name or selector used to match resource. The Tracker#TrackObject method is a convenience method to convert a client.Object into a tracker.Reference. Similar to the existing Config#TrackAndGet method, there is a new Config#TrackAndList method that follows the contract for client.List while also tracking resources matching the query. Direct users of the Tracker will need to adapt to the new interface, however, indirect uses (like reconciler tests) are preserved. Signed-off-by: Scott Andrews --- README.md | 4 +- reconcilers/enqueuer.go | 21 +- reconcilers/reconcilers.go | 61 +++- reconcilers/reconcilers_test.go | 148 +++++++++- reconcilers/reconcilers_validate_test.go | 8 +- testing/config.go | 10 +- testing/config_test.go | 12 +- testing/tracker.go | 94 +++++-- tracker/doc.go | 25 ++ tracker/enqueue.go | 270 ++++++++++++++++++ tracker/enqueue_test.go | 339 +++++++++++++++++++++++ tracker/interface.go | 84 ++++++ tracker/tracker.go | 171 ------------ 13 files changed, 1014 insertions(+), 233 deletions(-) create mode 100644 tracker/doc.go create mode 100644 tracker/enqueue.go create mode 100644 tracker/enqueue_test.go create mode 100644 tracker/interface.go delete mode 100644 tracker/tracker.go diff --git a/README.md b/README.md index f1f1721..95d511c 100644 --- a/README.md +++ b/README.md @@ -767,7 +767,9 @@ func StashExampleSubReconciler(c reconcilers.Config) reconcilers.SubReconciler { The [`Tracker`](https://pkg.go.dev/github.com/vmware-labs/reconciler-runtime/tracker#Tracker) provides a means for one resource to watch another resource for mutations, triggering the reconciliation of the resource defining the reference. -It's common to work with a resource that is also tracked. The [Config.TrackAndGet](https://pkg.go.dev/github.com/vmware-labs/reconciler-runtime/reconcilers#Config.TrackAndGet) method uses the same signature as client.Get, but additionally tracks the resource. +Resources can either be tracked by name or with a label selector using [`TrackReference`](https://pkg.go.dev/github.com/vmware-labs/reconciler-runtime/tracker#Tracker.TrackReference). + +It's common to work with a resource that is also tracked. The [Config.TrackAndGet](https://pkg.go.dev/github.com/vmware-labs/reconciler-runtime/reconcilers#Config.TrackAndGet) method uses the same signature as client.Get, but additionally tracks the resource. Likewise, the [Config.TrackAndList](https://pkg.go.dev/github.com/vmware-labs/reconciler-runtime/reconcilers#Config.TrackAndList) method uses the same signature as client.List, but additionally tracks resources matching the query. In the [Setup](https://pkg.go.dev/github.com/vmware-labs/reconciler-runtime/reconcilers#SyncReconciler) method, a watch is created that will notify the handler every time a resource of that kind is mutated. The [EnqueueTracked](https://pkg.go.dev/github.com/vmware-labs/reconciler-runtime/reconcilers#EnqueueTracked) helper returns a list of resources that are tracking the given resource, those resources are enqueued for the reconciler. diff --git a/reconcilers/enqueuer.go b/reconcilers/enqueuer.go index 7ed3033..5efe677 100644 --- a/reconcilers/enqueuer.go +++ b/reconcilers/enqueuer.go @@ -8,29 +8,26 @@ package reconcilers import ( "context" - "k8s.io/apimachinery/pkg/types" + "github.com/go-logr/logr" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/handler" - - "github.com/vmware-labs/reconciler-runtime/tracker" ) -func EnqueueTracked(ctx context.Context, by client.Object) handler.EventHandler { +func EnqueueTracked(ctx context.Context) handler.EventHandler { c := RetrieveConfigOrDie(ctx) + log := logr.FromContextOrDiscard(ctx) + return handler.EnqueueRequestsFromMapFunc( - func(a client.Object) []Request { + func(obj client.Object) []Request { var requests []Request - gvks, _, err := c.Scheme().ObjectKinds(by) + items, err := c.Tracker.GetObservers(obj) if err != nil { - panic(err) + log.Error(err, "unable to get tracked requests") + return nil } - key := tracker.NewKey( - gvks[0], - types.NamespacedName{Namespace: a.GetNamespace(), Name: a.GetName()}, - ) - for _, item := range c.Tracker.Lookup(ctx, key) { + for _, item := range items { requests = append(requests, Request{NamespacedName: item}) } diff --git a/reconcilers/reconcilers.go b/reconcilers/reconcilers.go index 71ae938..ddf9f9b 100644 --- a/reconcilers/reconcilers.go +++ b/reconcilers/reconcilers.go @@ -11,6 +11,7 @@ import ( "errors" "fmt" "reflect" + "strings" "sync" "time" @@ -21,11 +22,13 @@ import ( 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/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/cache" "k8s.io/client-go/tools/record" + "k8s.io/client-go/tools/reference" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" @@ -71,21 +74,56 @@ func (c Config) WithCluster(cluster cluster.Cluster) Config { // TrackAndGet tracks the resources for changes and returns the current value. The track is // registered even when the resource does not exists so that its creation can be tracked. // -// Equivalent to calling both `c.Tracker.Track(...)` and `c.Client.Get(...)` +// Equivalent to calling both `c.Tracker.TrackObject(...)` and `c.Client.Get(...)` func (c Config) TrackAndGet(ctx context.Context, key types.NamespacedName, obj client.Object, opts ...client.GetOption) error { - c.Tracker.Track( - ctx, - tracker.NewKey(gvk(obj, c.Scheme()), key), - RetrieveRequest(ctx).NamespacedName, - ) + // create synthetic resource to track from known type and request + req := RetrieveRequest(ctx) + resource := RetrieveResourceType(ctx).DeepCopyObject().(client.Object) + resource.SetNamespace(req.Namespace) + resource.SetName(req.Name) + ref := obj.DeepCopyObject().(client.Object) + ref.SetNamespace(key.Namespace) + ref.SetName(key.Name) + c.Tracker.TrackObject(ref, resource) + return c.Get(ctx, key, obj, opts...) } +// TrackAndList tracks the resources for changes and returns the current value. +// +// Equivalent to calling both `c.Tracker.TrackReference(...)` and `c.Client.List(...)` +func (c Config) TrackAndList(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { + // create synthetic resource to track from known type and request + req := RetrieveRequest(ctx) + resource := RetrieveResourceType(ctx).DeepCopyObject().(client.Object) + resource.SetNamespace(req.Namespace) + resource.SetName(req.Name) + + or, err := reference.GetReference(c.Scheme(), list) + if err != nil { + return err + } + gvk := schema.FromAPIVersionAndKind(or.APIVersion, or.Kind) + listOpts := (&client.ListOptions{}).ApplyOptions(opts) + if listOpts.LabelSelector == nil { + listOpts.LabelSelector = labels.Everything() + } + ref := tracker.Reference{ + APIGroup: gvk.Group, + Kind: strings.TrimSuffix(gvk.Kind, "List"), + Namespace: listOpts.Namespace, + Selector: listOpts.LabelSelector, + } + c.Tracker.TrackReference(ref, resource) + + return c.List(ctx, list, opts...) +} + // NewConfig creates a Config for a specific API type. Typically passed into a // reconciler. func NewConfig(mgr ctrl.Manager, apiType client.Object, syncPeriod time.Duration) Config { return Config{ - Tracker: tracker.New(2 * syncPeriod), + Tracker: tracker.New(mgr.GetScheme(), 2*syncPeriod), }.WithCluster(mgr) } @@ -601,7 +639,7 @@ func (r *AggregateReconciler[T]) Reconcile(ctx context.Context, req Request) (Re Client: c.Client, APIReader: c.APIReader, Recorder: c.Recorder, - Tracker: tracker.New(0), + Tracker: tracker.New(c.Scheme(), 0), }) desired, err := r.desiredResource(ctx, resource) if err != nil { @@ -1069,7 +1107,7 @@ func (r *ChildReconciler[T, CT, CLT]) SetupWithManager(ctx context.Context, mgr } if r.SkipOwnerReference { - bldr.Watches(&source.Kind{Type: r.ChildType}, EnqueueTracked(ctx, r.ChildType)) + bldr.Watches(&source.Kind{Type: r.ChildType}, EnqueueTracked(ctx)) } else { bldr.Owns(r.ChildType) } @@ -1680,7 +1718,8 @@ func (r *ResourceManager[T]) Manage(ctx context.Context, resource client.Object, if r.TrackDesired { // normally tracks should occur before API operations, but when creating a resource with a // generated name, we need to know the actual resource name. - if err := c.Tracker.TrackChild(ctx, resource, desired, c.Scheme()); err != nil { + + if err := c.Tracker.TrackObject(desired, resource); err != nil { return nilT, err } } @@ -1715,7 +1754,7 @@ func (r *ResourceManager[T]) Manage(ctx context.Context, resource client.Object, } log.Info("updating resource", "diff", cmp.Diff(r.sanitize(actual), r.sanitize(current))) if r.TrackDesired { - if err := c.Tracker.TrackChild(ctx, resource, current, c.Scheme()); err != nil { + if err := c.Tracker.TrackObject(current, resource); err != nil { return nilT, err } } diff --git a/reconcilers/reconcilers_test.go b/reconcilers/reconcilers_test.go index 8ea1d37..51a81da 100644 --- a/reconcilers/reconcilers_test.go +++ b/reconcilers/reconcilers_test.go @@ -25,6 +25,7 @@ import ( 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/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" @@ -122,6 +123,151 @@ func TestConfig_TrackAndGet(t *testing.T) { }) } +func TestConfig_TrackAndList(t *testing.T) { + testNamespace := "test-namespace" + testName := "test-resource" + testSelector, _ := labels.Parse("app=test-app") + + scheme := runtime.NewScheme() + _ = resources.AddToScheme(scheme) + _ = clientgoscheme.AddToScheme(scheme) + + resource := dies.TestResourceBlank. + MetadataDie(func(d *diemetav1.ObjectMetaDie) { + d.Namespace(testNamespace) + d.Name(testName) + }) + + configMap := diecorev1.ConfigMapBlank. + MetadataDie(func(d *diemetav1.ObjectMetaDie) { + d.Namespace("track-namespace") + d.Name("track-name") + d.AddLabel("app", "test-app") + }). + AddData("greeting", "hello") + + rts := rtesting.SubReconcilerTests[*resources.TestResource]{ + "track and list": { + Resource: resource.DieReleasePtr(), + GivenObjects: []client.Object{ + configMap, + }, + Metadata: map[string]interface{}{ + "listOpts": []client.ListOption{}, + }, + ExpectTracks: []rtesting.TrackRequest{ + { + Tracker: types.NamespacedName{ + Namespace: testNamespace, + Name: testName, + }, + TrackedReference: tracker.Reference{ + Kind: "ConfigMap", + Selector: labels.Everything(), + }, + }, + }, + }, + "track and list constrained": { + Resource: resource.DieReleasePtr(), + GivenObjects: []client.Object{ + configMap, + }, + Metadata: map[string]interface{}{ + "listOpts": []client.ListOption{ + client.InNamespace("track-namespace"), + client.MatchingLabels(map[string]string{"app": "test-app"}), + }, + }, + ExpectTracks: []rtesting.TrackRequest{ + { + Tracker: types.NamespacedName{ + Namespace: testNamespace, + Name: testName, + }, + TrackedReference: tracker.Reference{ + Kind: "ConfigMap", + Namespace: "track-namespace", + Selector: testSelector, + }, + }, + }, + }, + "track with errored list": { + Resource: resource.DieReleasePtr(), + ShouldErr: true, + WithReactors: []rtesting.ReactionFunc{ + rtesting.InduceFailure("list", "ConfigMapList"), + }, + Metadata: map[string]interface{}{ + "listOpts": []client.ListOption{}, + }, + ExpectTracks: []rtesting.TrackRequest{ + { + Tracker: types.NamespacedName{ + Namespace: testNamespace, + Name: testName, + }, + TrackedReference: tracker.Reference{ + Kind: "ConfigMap", + Selector: labels.Everything(), + }, + }, + }, + }, + } + + // run with typed objects + t.Run("typed", func(t *testing.T) { + rts.Run(t, scheme, func(t *testing.T, rtc *rtesting.SubReconcilerTestCase[*resources.TestResource], c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return &reconcilers.SyncReconciler[*resources.TestResource]{ + Sync: func(ctx context.Context, resource *resources.TestResource) error { + c := reconcilers.RetrieveConfigOrDie(ctx) + + cms := &corev1.ConfigMapList{} + listOpts := rtc.Metadata["listOpts"].([]client.ListOption) + err := c.TrackAndList(ctx, cms, listOpts...) + if err != nil { + return err + } + + if expected, actual := "hello", cms.Items[0].Data["greeting"]; expected != actual { + // should never get here + panic(fmt.Errorf("expected configmap to have greeting %q, found %q", expected, actual)) + } + return nil + }, + } + }) + }) + + // run with unstructured objects + t.Run("unstructured", func(t *testing.T) { + rts.Run(t, scheme, func(t *testing.T, rtc *rtesting.SubReconcilerTestCase[*resources.TestResource], c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { + return &reconcilers.SyncReconciler[*resources.TestResource]{ + Sync: func(ctx context.Context, resource *resources.TestResource) error { + c := reconcilers.RetrieveConfigOrDie(ctx) + + cms := &unstructured.UnstructuredList{} + cms.SetAPIVersion("v1") + cms.SetKind("ConfigMapList") + listOpts := rtc.Metadata["listOpts"].([]client.ListOption) + err := c.TrackAndList(ctx, cms, listOpts...) + if err != nil { + return err + } + + if expected, actual := "hello", cms.UnstructuredContent()["items"].([]interface{})[0].(map[string]interface{})["data"].(map[string]interface{})["greeting"].(string); expected != actual { + // should never get here + panic(fmt.Errorf("expected configmap to have greeting %q, found %q", expected, actual)) + } + return nil + }, + } + }) + }) +} + func TestResourceReconciler_NoStatus(t *testing.T) { testNamespace := "test-namespace" testName := "test-resource-no-status" @@ -3807,7 +3953,7 @@ func TestWithConfig(t *testing.T) { Metadata: map[string]interface{}{ "SubReconciler": func(t *testing.T, oc reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] { c := reconcilers.Config{ - Tracker: tracker.New(0), + Tracker: tracker.New(oc.Scheme(), 0), } return &reconcilers.WithConfig[*resources.TestResource]{ diff --git a/reconcilers/reconcilers_validate_test.go b/reconcilers/reconcilers_validate_test.go index 6545b3f..5b8a1e0 100644 --- a/reconcilers/reconcilers_validate_test.go +++ b/reconcilers/reconcilers_validate_test.go @@ -14,7 +14,9 @@ import ( "github.com/vmware-labs/reconciler-runtime/internal/resources" "github.com/vmware-labs/reconciler-runtime/tracker" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -545,8 +547,12 @@ func TestCastResource_validate(t *testing.T) { } func TestWithConfig_validate(t *testing.T) { + scheme := runtime.NewScheme() + _ = resources.AddToScheme(scheme) + _ = clientgoscheme.AddToScheme(scheme) + config := Config{ - Tracker: tracker.New(0), + Tracker: tracker.New(scheme, 0), } tests := []struct { diff --git a/testing/config.go b/testing/config.go index d710b31..e7e3afc 100644 --- a/testing/config.go +++ b/testing/config.go @@ -102,7 +102,7 @@ func (c *ExpectConfig) init() { events: []Event{}, scheme: c.Scheme, } - c.tracker = createTracker(c.GivenTracks) + c.tracker = createTracker(c.GivenTracks, c.Scheme) c.observedErrors = []string{} }) } @@ -330,18 +330,20 @@ func (c *ExpectConfig) AssertTrackerExpectations(t *testing.T) { actualTracks := c.tracker.getTrackRequests() for i, exp := range c.ExpectTracks { + exp.normalize() + if i >= len(actualTracks) { - c.errorf(t, "Missing tracking request for config %q: %s", c.Name, exp) + c.errorf(t, "Missing tracking request for config %q: %v", c.Name, exp) continue } - if diff := cmp.Diff(exp, actualTracks[i]); diff != "" { + if diff := cmp.Diff(exp, actualTracks[i], NormalizeLabelSelector); diff != "" { c.errorf(t, "Unexpected tracking request for config %q (-expected, +actual): %s", c.Name, diff) } } if actual, exp := len(actualTracks), len(c.ExpectTracks); actual > exp { for _, extra := range actualTracks[exp:] { - c.errorf(t, "Extra tracking request for config %q: %s", c.Name, extra) + c.errorf(t, "Extra tracking request for config %q: %v", c.Name, extra) } } } diff --git a/testing/config_test.go b/testing/config_test.go index 3d7e1f7..7d25e2d 100644 --- a/testing/config_test.go +++ b/testing/config_test.go @@ -171,7 +171,7 @@ func TestExpectConfig(t *testing.T) { }, }, operation: func(t *testing.T, ctx context.Context, c reconcilers.Config) { - actual := c.Tracker.Lookup(ctx, NewTrackRequest(r2, r1, scheme).Tracked) + actual, _ := c.Tracker.GetObservers(r2) expected := []types.NamespacedName{ {Namespace: r1.Namespace, Name: r1.Name}, } @@ -188,7 +188,7 @@ func TestExpectConfig(t *testing.T) { }, }, operation: func(t *testing.T, ctx context.Context, c reconcilers.Config) { - c.Tracker.TrackChild(ctx, r1, r2, scheme) + c.Tracker.TrackObject(r2, r1) }, failedAssertions: []string{}, }, @@ -199,7 +199,7 @@ func TestExpectConfig(t *testing.T) { }, }, operation: func(t *testing.T, ctx context.Context, c reconcilers.Config) { - c.Tracker.TrackChild(ctx, r1, r2, scheme) + c.Tracker.TrackObject(r2, r1) }, failedAssertions: []string{ `Unexpected tracking request for config "test" (-expected, +actual): `, @@ -210,10 +210,10 @@ func TestExpectConfig(t *testing.T) { ExpectTracks: []TrackRequest{}, }, operation: func(t *testing.T, ctx context.Context, c reconcilers.Config) { - c.Tracker.TrackChild(ctx, r1, r2, scheme) + c.Tracker.TrackObject(r2, r1) }, failedAssertions: []string{ - `Extra tracking request for config "test": {my-namespace/resource-1 {TestResource.testing.reconciler.runtime my-namespace/resource-2}}`, + `Extra tracking request for config "test": {my-namespace/resource-1 { /} {testing.reconciler.runtime TestResource my-namespace resource-2 }}`, }, }, "missing track": { @@ -224,7 +224,7 @@ func TestExpectConfig(t *testing.T) { }, operation: func(t *testing.T, ctx context.Context, c reconcilers.Config) {}, failedAssertions: []string{ - `Missing tracking request for config "test": {my-namespace/resource-1 {TestResource.testing.reconciler.runtime my-namespace/resource-2}}`, + `Missing tracking request for config "test": {my-namespace/resource-1 { /} {testing.reconciler.runtime TestResource my-namespace resource-2 }}`, }, }, diff --git a/testing/tracker.go b/testing/tracker.go index f1fef3a..24297f6 100644 --- a/testing/tracker.go +++ b/testing/tracker.go @@ -6,14 +6,14 @@ SPDX-License-Identifier: Apache-2.0 package testing import ( - "context" - "fmt" "time" "github.com/vmware-labs/reconciler-runtime/tracker" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/reference" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -21,8 +21,26 @@ import ( type TrackRequest struct { // Tracker is the object doing the tracking Tracker types.NamespacedName + + // Deprecated use TrackedReference // Tracked is the object being tracked Tracked tracker.Key + + // TrackedReference is a ref to the object being tracked + TrackedReference tracker.Reference +} + +func (tr *TrackRequest) normalize() { + if tr.TrackedReference != (tracker.Reference{}) { + return + } + tr.TrackedReference = tracker.Reference{ + APIGroup: tr.Tracked.GroupKind.Group, + Kind: tr.Tracked.GroupKind.Kind, + Namespace: tr.Tracked.NamespacedName.Namespace, + Name: tr.Tracked.NamespacedName.Name, + } + tr.Tracked = tracker.Key{} } type trackBy func(trackingObjNamespace, trackingObjName string) TrackRequest @@ -34,7 +52,12 @@ func (t trackBy) By(trackingObjNamespace, trackingObjName string) TrackRequest { func CreateTrackRequest(trackedObjGroup, trackedObjKind, trackedObjNamespace, trackedObjName string) trackBy { return func(trackingObjNamespace, trackingObjName string) TrackRequest { return TrackRequest{ - Tracked: tracker.Key{GroupKind: schema.GroupKind{Group: trackedObjGroup, Kind: trackedObjKind}, NamespacedName: types.NamespacedName{Namespace: trackedObjNamespace, Name: trackedObjName}}, + TrackedReference: tracker.Reference{ + APIGroup: trackedObjGroup, + Kind: trackedObjKind, + Namespace: trackedObjNamespace, + Name: trackedObjName, + }, Tracker: types.NamespacedName{Namespace: trackingObjNamespace, Name: trackingObjName}, } } @@ -47,17 +70,27 @@ func NewTrackRequest(t, b client.Object, scheme *runtime.Scheme) TrackRequest { panic(err) } return TrackRequest{ - Tracked: tracker.Key{GroupKind: schema.GroupKind{Group: gvks[0].Group, Kind: gvks[0].Kind}, NamespacedName: types.NamespacedName{Namespace: tracked.GetNamespace(), Name: tracked.GetName()}}, + TrackedReference: tracker.Reference{ + APIGroup: gvks[0].Group, + Kind: gvks[0].Kind, + Namespace: tracked.GetNamespace(), + Name: tracked.GetName(), + }, Tracker: types.NamespacedName{Namespace: by.GetNamespace(), Name: by.GetName()}, } } -const maxDuration = time.Duration(1<<63 - 1) - -func createTracker(given []TrackRequest) *mockTracker { - t := &mockTracker{Tracker: tracker.New(maxDuration)} +func createTracker(given []TrackRequest, scheme *runtime.Scheme) *mockTracker { + t := &mockTracker{ + Tracker: tracker.New(scheme, 24*time.Hour), + scheme: scheme, + } for _, g := range given { - t.Track(context.TODO(), g.Tracked, g.Tracker) + g.normalize() + obj := &unstructured.Unstructured{} + obj.SetNamespace(g.Tracker.Namespace) + obj.SetName(g.Tracker.Name) + t.TrackReference(g.TrackedReference, obj) } // reset tracked requests t.reqs = []TrackRequest{} @@ -66,30 +99,39 @@ func createTracker(given []TrackRequest) *mockTracker { type mockTracker struct { tracker.Tracker - reqs []TrackRequest + reqs []TrackRequest + scheme *runtime.Scheme } var _ tracker.Tracker = &mockTracker{} -func (t *mockTracker) Track(ctx context.Context, ref tracker.Key, obj types.NamespacedName) { - t.Tracker.Track(ctx, ref, obj) - t.reqs = append(t.reqs, TrackRequest{Tracked: ref, Tracker: obj}) -} - -func (t *mockTracker) TrackChild(ctx context.Context, parent, child client.Object, s *runtime.Scheme) error { - gvks, _, err := s.ObjectKinds(child) +// TrackObject tells us that "obj" is tracking changes to the +// referenced object. +func (t *mockTracker) TrackObject(ref client.Object, obj client.Object) error { + or, err := reference.GetReference(t.scheme, ref) if err != nil { return err } - if len(gvks) != 1 { - return fmt.Errorf("expected exactly one GVK, found: %s", gvks) - } - t.Track( - ctx, - tracker.NewKey(gvks[0], types.NamespacedName{Namespace: child.GetNamespace(), Name: child.GetName()}), - types.NamespacedName{Namespace: parent.GetNamespace(), Name: parent.GetName()}, - ) - return nil + gv := schema.FromAPIVersionAndKind(or.APIVersion, or.Kind) + return t.TrackReference(tracker.Reference{ + APIGroup: gv.Group, + Kind: gv.Kind, + Namespace: ref.GetNamespace(), + Name: ref.GetName(), + }, obj) +} + +// TrackReference tells us that "obj" is tracking changes to the +// referenced object. +func (t *mockTracker) TrackReference(ref tracker.Reference, obj client.Object) error { + t.reqs = append(t.reqs, TrackRequest{ + Tracker: types.NamespacedName{ + Namespace: obj.GetNamespace(), + Name: obj.GetName(), + }, + TrackedReference: ref, + }) + return t.Tracker.TrackReference(ref, obj) } func (t *mockTracker) getTrackRequests() []TrackRequest { diff --git a/tracker/doc.go b/tracker/doc.go new file mode 100644 index 0000000..db71fb2 --- /dev/null +++ b/tracker/doc.go @@ -0,0 +1,25 @@ +/* +Copyright 2018 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package tracker defines a utility to enable Reconcilers to trigger +// reconciliations when objects that are cross-referenced change, so +// that the level-based reconciliation can react to the change. The +// prototypical cross-reference in Kubernetes is corev1.ObjectReference. +// +// Imported from https://github.com/knative/pkg/tree/db8a35330281c41c7e8e90df6059c23a36af0643/tracker +// liberated from Knative runtime dependencies and evolved to better +// fit controller-runtime patterns. +package tracker diff --git a/tracker/enqueue.go b/tracker/enqueue.go new file mode 100644 index 0000000..4066dd1 --- /dev/null +++ b/tracker/enqueue.go @@ -0,0 +1,270 @@ +/* +Copyright 2018 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package tracker + +import ( + "fmt" + "sort" + "strings" + "sync" + "time" + + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apimachinery/pkg/util/validation" + "k8s.io/client-go/tools/reference" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// New returns an implementation of Interface that lets a Reconciler +// register a particular resource as watching an ObjectReference for +// a particular lease duration. This watch must be refreshed +// periodically (e.g. by a controller resync) or it will expire. +func New(scheme *runtime.Scheme, lease time.Duration) Tracker { + return &impl{ + scheme: scheme, + leaseDuration: lease, + } +} + +type impl struct { + m sync.Mutex + // exact maps from an object reference to the set of + // keys for objects watching it. + exact map[Reference]exactSet + // inexact maps from a partial object reference (no name/selector) to + // a map from watcher keys to the compiled selector and expiry. + inexact map[Reference]inexactSet + + // scheme used to convert typed objects to GVKs + scheme *runtime.Scheme + // The amount of time that an object may watch another + // before having to renew the lease. + leaseDuration time.Duration +} + +// Check that impl implements Interface. +var _ Tracker = (*impl)(nil) + +// exactSet is a map from keys to expirations. +type exactSet map[types.NamespacedName]time.Time + +// inexactSet is a map from keys to matchers. +type inexactSet map[types.NamespacedName]matchers + +// matchers is a map from matcherKeys to matchers +type matchers map[matcherKey]matcher + +// matcher holds the selector and expiry for matching tracked objects. +type matcher struct { + // The selector to complete the match. + selector labels.Selector + + // When this lease expires. + expiry time.Time +} + +// matcherKey holds a stringified selector and namespace. +type matcherKey struct { + // The selector for the matcher stringified + selector string + + // The namespace to complete the match. Empty matches cluster scope + // and all namespaced resources. + namespace string +} + +// Track implements Interface. +func (i *impl) TrackObject(ref client.Object, obj client.Object) error { + or, err := reference.GetReference(i.scheme, ref) + if err != nil { + return err + } + + return i.TrackReference(Reference{ + APIGroup: or.APIVersion, + Kind: or.Kind, + Namespace: or.Namespace, + Name: or.Name, + }, obj) +} + +func (i *impl) TrackReference(ref Reference, obj client.Object) error { + invalidFields := map[string][]string{ + "Kind": validation.IsCIdentifier(ref.Kind), + } + // Allow apiGroup to be empty for core resources + if ref.APIGroup != "" { + invalidFields["APIGroup"] = validation.IsDNS1123Subdomain(ref.APIGroup) + } + // Allow namespace to be empty for cluster-scoped references. + if ref.Namespace != "" { + invalidFields["Namespace"] = validation.IsDNS1123Label(ref.Namespace) + } + fieldErrors := []string{} + switch { + case ref.Selector != nil && ref.Name != "": + fieldErrors = append(fieldErrors, "cannot provide both Name and Selector") + case ref.Name != "": + invalidFields["Name"] = validation.IsDNS1123Subdomain(ref.Name) + case ref.Selector != nil: + default: + fieldErrors = append(fieldErrors, "must provide either Name or Selector") + } + for k, v := range invalidFields { + for _, msg := range v { + fieldErrors = append(fieldErrors, fmt.Sprintf("%s: %s", k, msg)) + } + } + if len(fieldErrors) > 0 { + sort.Strings(fieldErrors) + return fmt.Errorf("invalid Reference:\n%s", strings.Join(fieldErrors, "\n")) + } + + key := types.NamespacedName{Namespace: obj.GetNamespace(), Name: obj.GetName()} + + i.m.Lock() + defer i.m.Unlock() + + if i.exact == nil { + i.exact = make(map[Reference]exactSet) + } + if i.inexact == nil { + i.inexact = make(map[Reference]inexactSet) + } + + // If the reference uses Name then it is an exact match. + if ref.Selector == nil { + l, ok := i.exact[ref] + if !ok { + l = exactSet{} + } + + // Overwrite the key with a new expiration. + l[key] = time.Now().Add(i.leaseDuration) + + i.exact[ref] = l + return nil + } + + // Otherwise, it is an inexact match by selector. + partialRef := Reference{ + APIGroup: ref.APIGroup, + Kind: ref.Kind, + // Exclude the namespace and selector, they are captured in the matcher. + } + is, ok := i.inexact[partialRef] + if !ok { + is = inexactSet{} + } + m, ok := is[key] + if !ok { + m = matchers{} + } + + // Overwrite the key with a new expiration. + m[matcherKey{ + selector: ref.Selector.String(), + namespace: ref.Namespace, + }] = matcher{ + selector: ref.Selector, + expiry: time.Now().Add(i.leaseDuration), + } + + is[key] = m + + i.inexact[partialRef] = is + return nil +} + +// GetObservers implements Interface. +func (i *impl) GetObservers(obj client.Object) ([]types.NamespacedName, error) { + or, err := reference.GetReference(i.scheme, obj) + if err != nil { + return nil, err + } + + gv, err := schema.ParseGroupVersion(or.APIVersion) + if err != nil { + return nil, err + } + ref := Reference{ + APIGroup: gv.Group, + Kind: or.Kind, + Namespace: or.Namespace, + Name: or.Name, + } + + keys := sets.Set[types.NamespacedName]{} + + i.m.Lock() + defer i.m.Unlock() + + now := time.Now() + + // Handle exact matches. + s, ok := i.exact[ref] + if ok { + for key, expiry := range s { + // If the expiration has lapsed, then delete the key. + if now.After(expiry) { + delete(s, key) + continue + } + keys.Insert(key) + } + if len(s) == 0 { + delete(i.exact, ref) + } + } + + // Handle inexact matches. + ref.Name = "" + ref.Namespace = "" + is, ok := i.inexact[ref] + if ok { + ls := labels.Set(obj.GetLabels()) + for key, ms := range is { + for k, m := range ms { + // If the expiration has lapsed, then delete the key. + if now.After(m.expiry) { + delete(ms, k) + continue + } + // Match namespace, allowing for a cluster wide match. + if k.namespace != "" && k.namespace != obj.GetNamespace() { + continue + } + if !m.selector.Matches(ls) { + continue + } + keys.Insert(key) + } + if len(ms) == 0 { + delete(is, key) + } + } + if len(is) == 0 { + delete(i.inexact, ref) + } + } + + return keys.UnsortedList(), nil +} diff --git a/tracker/enqueue_test.go b/tracker/enqueue_test.go new file mode 100644 index 0000000..6bd4169 --- /dev/null +++ b/tracker/enqueue_test.go @@ -0,0 +1,339 @@ +/* +Copyright 2023 VMware, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ + +package tracker_test + +import ( + "sort" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/vmware-labs/reconciler-runtime/internal/resources" + "github.com/vmware-labs/reconciler-runtime/tracker" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func TestTracker(t *testing.T) { + scheme := runtime.NewScheme() + _ = resources.AddToScheme(scheme) + _ = clientgoscheme.AddToScheme(scheme) + + referrer := &resources.TestResource{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-namespace", + Name: "test-name", + }, + } + referrerOther := &resources.TestResource{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-namespace", + Name: "other-name", + }, + } + + referent := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-namespace", + Name: "test-name", + Labels: map[string]string{ + "app": "test", + }, + }, + } + + type track struct { + ref tracker.Reference + obj client.Object + } + + tests := map[string]struct { + lease time.Duration + tracks []track + obj client.Object + expected []types.NamespacedName + }{ + "empty tracker matches nothing": { + lease: time.Hour, + obj: referent, + expected: []types.NamespacedName{}, + }, + "match by name": { + lease: time.Hour, + tracks: []track{ + { + ref: tracker.Reference{ + APIGroup: "", + Kind: "ConfigMap", + Namespace: "test-namespace", + Name: "test-name", + }, + obj: referrer, + }, + }, + obj: referent, + expected: []types.NamespacedName{ + {Namespace: "test-namespace", Name: "test-name"}, + }, + }, + "multiple matches by name": { + lease: time.Hour, + tracks: []track{ + { + ref: tracker.Reference{ + APIGroup: "", + Kind: "ConfigMap", + Namespace: "test-namespace", + Name: "test-name", + }, + obj: referrer, + }, + { + ref: tracker.Reference{ + APIGroup: "", + Kind: "ConfigMap", + Namespace: "test-namespace", + Name: "test-name", + }, + obj: referrerOther, + }, + }, + obj: referent, + expected: []types.NamespacedName{ + {Namespace: "test-namespace", Name: "other-name"}, + {Namespace: "test-namespace", Name: "test-name"}, + }, + }, + "does not match other names": { + lease: time.Hour, + tracks: []track{ + { + ref: tracker.Reference{ + APIGroup: "", + Kind: "ConfigMap", + Namespace: "test-namespace", + Name: "other-name", + }, + obj: referrer, + }, + }, + obj: referent, + expected: []types.NamespacedName{}, + }, + "does not match other namespaces": { + lease: time.Hour, + tracks: []track{ + { + ref: tracker.Reference{ + APIGroup: "", + Kind: "ConfigMap", + Namespace: "other-namespace", + Name: "test-name", + }, + obj: referrer, + }, + }, + obj: referent, + expected: []types.NamespacedName{}, + }, + "does not match other groups": { + lease: time.Hour, + tracks: []track{ + { + ref: tracker.Reference{ + APIGroup: "fake", + Kind: "ConfigMap", + Namespace: "test-namespace", + Name: "test-name", + }, + obj: referrer, + }, + }, + obj: referent, + expected: []types.NamespacedName{}, + }, + "does not match other kinds": { + lease: time.Hour, + tracks: []track{ + { + ref: tracker.Reference{ + APIGroup: "", + Kind: "Secret", + Namespace: "test-namespace", + Name: "test-name", + }, + obj: referrer, + }, + }, + obj: referent, + expected: []types.NamespacedName{}, + }, + "match by selector": { + lease: time.Hour, + tracks: []track{ + { + ref: tracker.Reference{ + APIGroup: "", + Kind: "ConfigMap", + Selector: labels.SelectorFromSet(map[string]string{ + "app": "test", + }), + }, + obj: referrer, + }, + }, + obj: referent, + expected: []types.NamespacedName{ + {Namespace: "test-namespace", Name: "test-name"}, + }, + }, + "match by selector in namespace": { + lease: time.Hour, + tracks: []track{ + { + ref: tracker.Reference{ + APIGroup: "", + Kind: "ConfigMap", + Namespace: "test-namespace", + Selector: labels.SelectorFromSet(map[string]string{ + "app": "test", + }), + }, + obj: referrer, + }, + }, + obj: referent, + expected: []types.NamespacedName{ + {Namespace: "test-namespace", Name: "test-name"}, + }, + }, + "no match by selector in wrong namespace": { + lease: time.Hour, + tracks: []track{ + { + ref: tracker.Reference{ + APIGroup: "", + Kind: "ConfigMap", + Namespace: "other-namespace", + Selector: labels.SelectorFromSet(map[string]string{ + "app": "test", + }), + }, + obj: referrer, + }, + }, + obj: referent, + expected: []types.NamespacedName{}, + }, + "no match by selector missing label": { + lease: time.Hour, + tracks: []track{ + { + ref: tracker.Reference{ + APIGroup: "", + Kind: "ConfigMap", + Namespace: "test-namespace", + Selector: labels.SelectorFromSet(map[string]string{ + "app": "other", + }), + }, + obj: referrer, + }, + }, + obj: referent, + expected: []types.NamespacedName{}, + }, + "multiple matches by selector": { + lease: time.Hour, + tracks: []track{ + { + ref: tracker.Reference{ + APIGroup: "", + Kind: "ConfigMap", + Selector: labels.SelectorFromSet(map[string]string{ + "app": "test", + }), + }, + obj: referrer, + }, + { + ref: tracker.Reference{ + APIGroup: "", + Kind: "ConfigMap", + Selector: labels.SelectorFromSet(map[string]string{ + "app": "test", + }), + }, + obj: referrerOther, + }, + }, + obj: referent, + expected: []types.NamespacedName{ + {Namespace: "test-namespace", Name: "other-name"}, + {Namespace: "test-namespace", Name: "test-name"}, + }, + }, + "no match by name for expired lease": { + lease: time.Nanosecond, + tracks: []track{ + { + ref: tracker.Reference{ + APIGroup: "", + Kind: "ConfigMap", + Namespace: "test-namespace", + Name: "test-name", + }, + obj: referrer, + }, + }, + obj: referent, + expected: []types.NamespacedName{}, + }, + "no match by selector for expired lease": { + lease: time.Nanosecond, + tracks: []track{ + { + ref: tracker.Reference{ + APIGroup: "", + Kind: "ConfigMap", + Selector: labels.SelectorFromSet(map[string]string{ + "app": "test", + }), + }, + obj: referrer, + }, + }, + obj: referent, + expected: []types.NamespacedName{}, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + tracker := tracker.New(scheme, tc.lease) + for _, track := range tc.tracks { + tracker.TrackReference(track.ref, track.obj) + } + + actual, _ := tracker.GetObservers(tc.obj) + sort.Slice(actual, func(i, j int) bool { + if in, jn := actual[i].Namespace, actual[j].Namespace; in != jn { + return in < jn + } + return actual[i].Name < actual[j].Name + }) + expected := tc.expected + if diff := cmp.Diff(expected, actual); diff != "" { + t.Errorf("expected observers to match actual observers: %s", diff) + } + }) + } +} diff --git a/tracker/interface.go b/tracker/interface.go new file mode 100644 index 0000000..6b40122 --- /dev/null +++ b/tracker/interface.go @@ -0,0 +1,84 @@ +/* +Copyright 2018 The Knative Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package tracker + +import ( + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// Reference is modeled after corev1.ObjectReference, but omits fields +// unsupported by the tracker, and permits us to extend things in +// divergent ways. +// +// APIVersion is reduce to APIGroup as the version of a tracked object +// is irrelevant. +type Reference struct { + // APIGroup of the referent. + // +optional + APIGroup string + + // Kind of the referent. + // +optional + Kind string + + // Namespace of the referent. + // +optional + Namespace string + + // Name of the referent. + // Mutually exclusive with Selector. + // +optional + Name string + + // Selector of the referents. + // Mutually exclusive with Name. + // +optional + Selector labels.Selector +} + +// Tracker defines the interface through which an object can register +// that it is tracking another object by reference. +type Tracker interface { + // TrackReference tells us that "obj" is tracking changes to the + // referenced object. + TrackReference(ref Reference, obj client.Object) error + + // TrackObject tells us that "obj" is tracking changes to the + // referenced object. + TrackObject(ref client.Object, obj client.Object) error + + // GetObservers returns the names of all observers for the given + // object. + GetObservers(obj client.Object) ([]types.NamespacedName, error) +} + +// Deprecated: use Reference +func NewKey(gvk schema.GroupVersionKind, namespacedName types.NamespacedName) Key { + return Key{ + GroupKind: gvk.GroupKind(), + NamespacedName: namespacedName, + } +} + +// Deprecated: use Reference +type Key struct { + GroupKind schema.GroupKind + NamespacedName types.NamespacedName +} diff --git a/tracker/tracker.go b/tracker/tracker.go deleted file mode 100644 index ee67aa7..0000000 --- a/tracker/tracker.go +++ /dev/null @@ -1,171 +0,0 @@ -/* -Copyright 2018 The Knative Authors - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -/* -Copyright 2019-2020 VMware, Inc. -SPDX-License-Identifier: Apache-2.0 -*/ - -// modified from https://github.com/knative/pkg/tree/master/tracker - -package tracker - -import ( - "context" - "fmt" - "sync" - "time" - - "github.com/go-logr/logr" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -// Tracker defines the interface through which an object can register -// that it is tracking another object by reference. -type Tracker interface { - // Track tells us that "obj" is tracking changes to the - // referenced object. - Track(ctx context.Context, ref Key, obj types.NamespacedName) - - // TrackChild tracks the child by the parent. - TrackChild(ctx context.Context, parent, child client.Object, s *runtime.Scheme) error - - // Lookup returns actively tracked objects for the reference. - Lookup(ctx context.Context, ref Key) []types.NamespacedName -} - -func NewKey(gvk schema.GroupVersionKind, namespacedName types.NamespacedName) Key { - return Key{ - GroupKind: gvk.GroupKind(), - NamespacedName: namespacedName, - } -} - -type Key struct { - GroupKind schema.GroupKind - NamespacedName types.NamespacedName -} - -func (k *Key) String() string { - return fmt.Sprintf("%s/%s", k.GroupKind, k.NamespacedName) -} - -// New returns an implementation of Tracker that lets a Reconciler -// register a particular resource as watching a resource for -// a particular lease duration. This watch must be refreshed -// periodically (e.g. by a controller resync) or it will expire. -func New(lease time.Duration) Tracker { - return &impl{ - leaseDuration: lease, - } -} - -type impl struct { - m sync.Mutex - - // mapping maps from an object reference to the set of - // keys for objects watching it. - mapping map[string]set - - // The amount of time that an object may watch another - // before having to renew the lease. - leaseDuration time.Duration -} - -// Check that impl implements Tracker. -var _ Tracker = (*impl)(nil) - -// set is a map from keys to expirations -type set map[types.NamespacedName]time.Time - -// TrackChild implements Tracker. -func (i *impl) TrackChild(ctx context.Context, parent, child client.Object, s *runtime.Scheme) error { - gvks, _, err := s.ObjectKinds(child) - if err != nil { - return err - } - if len(gvks) != 1 { - return fmt.Errorf("expected exactly one GVK, found: %s", gvks) - } - i.Track( - ctx, - NewKey(gvks[0], types.NamespacedName{Namespace: child.GetNamespace(), Name: child.GetName()}), - types.NamespacedName{Namespace: parent.GetNamespace(), Name: parent.GetName()}, - ) - return nil -} - -// Track implements Tracker. -func (i *impl) Track(ctx context.Context, ref Key, obj types.NamespacedName) { - log := logr.FromContextOrDiscard(ctx).WithName("tracker") - - i.m.Lock() - defer i.m.Unlock() - if i.mapping == nil { - i.mapping = make(map[string]set) - } - - l, ok := i.mapping[ref.String()] - if !ok { - l = set{} - } - // Overwrite the key with a new expiration. - l[obj] = time.Now().Add(i.leaseDuration) - - i.mapping[ref.String()] = l - - log.Info("tracking resource", "ref", ref.String(), "obj", obj.String(), "ttl", l[obj].UTC().Format(time.RFC3339)) -} - -func isExpired(expiry time.Time) bool { - return time.Now().After(expiry) -} - -// Lookup implements Tracker. -func (i *impl) Lookup(ctx context.Context, ref Key) []types.NamespacedName { - log := logr.FromContextOrDiscard(ctx).WithName("tracker") - - items := []types.NamespacedName{} - - // TODO(mattmoor): Consider locking the mapping (global) for a - // smaller scope and leveraging a per-set lock to guard its access. - i.m.Lock() - defer i.m.Unlock() - s, ok := i.mapping[ref.String()] - if !ok { - log.V(2).Info("no tracked items found", "ref", ref.String()) - return items - } - - for key, expiry := range s { - // If the expiration has lapsed, then delete the key. - if isExpired(expiry) { - delete(s, key) - continue - } - items = append(items, key) - } - - if len(s) == 0 { - delete(i.mapping, ref.String()) - } - - log.V(1).Info("found tracked items", "ref", ref.String(), "items", items) - - return items -}