Skip to content

Commit

Permalink
TrackAndGet
Browse files Browse the repository at this point in the history
Streamline the flow for looking up a resource and watching it for
changes. The Tracker has long lived on the Config object as a peer to
the Client, it's very common to Track() a resource and then Get() that
same resource. Now we can do both, this effectively makes tracking
transparent as TrackAndGet() and Get() have the same method signature.

Before:

    c.Tracker.Track(
        ctx,
        tracker.NewKey(
           schema.GroupVersionKind{Version: "v1", Kind: "ServiceAccount"},
           types.NamespacedName{Namespace: parent.Namespace, Name: serviceAccountName},
        ),
        types.NamespacedName{Namespace: parent.Namespace, Name: parent.Name},
    )
    serviceAccount := corev1.ServiceAccount{}
    err := c.Get(ctx, types.NamespacedName{Namespace: parent.Namespace, Name: serviceAccountName}, &serviceAccount)

After:

    serviceAccount := corev1.ServiceAccount{}
    err := c.TrackAndGet(ctx, types.NamespacedName{Namespace: parent.Namespace, Name: serviceAccountName}, &serviceAccount)

The implementation assumes the ctx extends from a ParentReconciler.

Signed-off-by: Scott Andrews <andrewssc@vmware.com>
  • Loading branch information
scothis committed May 9, 2022
1 parent c7766dc commit d5a278f
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 10 deletions.
17 changes: 8 additions & 9 deletions README.md
Expand Up @@ -530,6 +530,12 @@ 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.

In the [Setup](https://pkg.go.dev/github.com/vmware-labs/reconciler-runtime@v0.4.0/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.

The tracker will automatically expire a track request if not periodically renewed. By default, the TTL is 2x the resync internal. This ensures all tracked resources will naturally have the tracking relationship refreshed as part of the normal reconciliation resource. There is no need to manually untrack a resource.

**Example:**

The stream gateways in projectriff fetch the image references they use to run from a ConfigMap, when the values change, we want to detect and rollout the updated images.
Expand All @@ -544,15 +550,8 @@ func InMemoryGatewaySyncConfigReconciler(c reconcilers.Config, namespace string)

var config corev1.ConfigMap
key := types.NamespacedName{Namespace: namespace, Name: inmemoryGatewayImages}
// track config for new images
c.Tracker.Track(
// the resource to track, GVK and NamespacedName
tracker.NewKey(schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"}, key),
// the resource to enqueue, NamespacedName only
types.NamespacedName{Namespace: parent.Namespace, Name: parent.Name},
)
// get the configmap
if err := c.Get(ctx, key, &config); err != nil {
// track config for new images, get the configmap
if err := c.TrackAndGet(ctx, key, &config); err != nil {
return err
}
// consume the configmap
Expand Down
29 changes: 28 additions & 1 deletion reconcilers/reconcilers.go
Expand Up @@ -69,14 +69,27 @@ 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.
//
// Equivlent to calling both `c.Tracker.Track(...)` and `c.Client.Get(...)`
func (c Config) TrackAndGet(ctx context.Context, key types.NamespacedName, obj client.Object) error {
c.Tracker.Track(
ctx,
tracker.NewKey(gvk(obj, c.Scheme()), key),
RetrieveRequest(ctx).NamespacedName,
)
return c.Get(ctx, key, obj)
}

// 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 {
name := typeName(apiType)
log := newWarnOnceLogger(ctrl.Log.WithName("controllers").WithName(name))
return Config{
Log: log,
Tracker: tracker.New(syncPeriod),
Tracker: tracker.New(2 * syncPeriod),
}.WithCluster(mgr)
}

Expand Down Expand Up @@ -135,6 +148,7 @@ func (r *ParentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
WithValues("parentType", gvk(r.Type, c.Scheme()))
ctx = logr.NewContext(ctx, log)

ctx = StashRequest(ctx, req)
ctx = StashConfig(ctx, c)
ctx = StashParentConfig(ctx, c)
ctx = StashParentType(ctx, r.Type)
Expand Down Expand Up @@ -253,11 +267,16 @@ func (r *ParentReconciler) syncLastTransitionTime(proposed, original []metav1.Co
}
}

const requestStashKey StashKey = "reconciler-runtime:request"
const configStashKey StashKey = "reconciler-runtime:config"
const parentConfigStashKey StashKey = "reconciler-runtime:parentConfig"
const parentTypeStashKey StashKey = "reconciler-runtime:parentType"
const castParentTypeStashKey StashKey = "reconciler-runtime:castParentType"

func StashRequest(ctx context.Context, req ctrl.Request) context.Context {
return context.WithValue(ctx, requestStashKey, req)
}

func StashConfig(ctx context.Context, config Config) context.Context {
return context.WithValue(ctx, configStashKey, config)
}
Expand All @@ -274,6 +293,14 @@ func StashCastParentType(ctx context.Context, currentType client.Object) context
return context.WithValue(ctx, castParentTypeStashKey, currentType)
}

func RetrieveRequest(ctx context.Context) ctrl.Request {
value := ctx.Value(requestStashKey)
if req, ok := value.(ctrl.Request); ok {
return req
}
return ctrl.Request{}
}

func RetrieveConfig(ctx context.Context) Config {
value := ctx.Value(configStashKey)
if config, ok := value.(Config); ok {
Expand Down
61 changes: 61 additions & 0 deletions reconcilers/reconcilers_test.go
Expand Up @@ -34,6 +34,67 @@ import (
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)

func TestConfig_TrackAndGet(t *testing.T) {
testNamespace := "test-namespace"
testName := "test-resource"

scheme := runtime.NewScheme()
_ = resources.AddToScheme(scheme)
_ = clientgoscheme.AddToScheme(scheme)

resource := dies.TestResourceBlank.
MetadataDie(func(d *diemetav1.ObjectMetaDie) {
d.Namespace(testNamespace)
d.Name(testName)
d.CreationTimestamp(metav1.NewTime(time.UnixMilli(1000)))
})

configMap := diecorev1.ConfigMapBlank.
MetadataDie(func(d *diemetav1.ObjectMetaDie) {
d.Namespace("track-namespace")
d.Name("track-name")
}).
AddData("greeting", "hello")

rts := rtesting.SubReconcilerTestSuite{{
Name: "track and get",
Parent: resource,
GivenObjects: []client.Object{
configMap,
},
ExpectTracks: []rtesting.TrackRequest{
rtesting.NewTrackRequest(configMap, resource, scheme),
},
}, {
Name: "track with not found get",
Parent: resource,
ShouldErr: true,
ExpectTracks: []rtesting.TrackRequest{
rtesting.NewTrackRequest(configMap, resource, scheme),
},
}}

rts.Test(t, scheme, func(t *testing.T, rtc *rtesting.SubReconcilerTestCase, c reconcilers.Config) reconcilers.SubReconciler {
return &reconcilers.SyncReconciler{
Sync: func(ctx context.Context, parent *resources.TestResource) error {
c := reconcilers.RetrieveConfig(ctx)

cm := &corev1.ConfigMap{}
err := c.TrackAndGet(ctx, types.NamespacedName{Namespace: "track-namespace", Name: "track-name"}, cm)
if err != nil {
return err
}

if expected, actual := "hello", cm.Data["greeting"]; expected != actual {
// should never get here
panic(fmt.Errorf("expected configmap to have greeting %q, found %q", expected, actual))
}
return nil
},
}
})
}

func TestParentReconcilerWithNoStatus(t *testing.T) {
testNamespace := "test-namespace"
testName := "test-resource-no-status"
Expand Down
4 changes: 4 additions & 0 deletions testing/subreconciler.go
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/vmware-labs/reconciler-runtime/reconcilers"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
controllerruntime "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
Expand Down Expand Up @@ -170,6 +171,9 @@ func (tc *SubReconcilerTestCase) Run(t *testing.T, scheme *runtime.Scheme, facto
// this value is also set by the test client when resource are added as givens
parent.SetResourceVersion("999")
}
ctx = reconcilers.StashRequest(ctx, reconcile.Request{
NamespacedName: types.NamespacedName{Namespace: parent.GetNamespace(), Name: parent.GetName()},
})
ctx = reconcilers.StashParentType(ctx, parent.DeepCopyObject().(client.Object))
ctx = reconcilers.StashCastParentType(ctx, parent.DeepCopyObject().(client.Object))

Expand Down

0 comments on commit d5a278f

Please sign in to comment.