Skip to content

Commit bae8fdb

Browse files
authored
Merge pull request #1174 from DirectXMan12/feature/metadata-only-watch
✨ metadata-only watches
2 parents 5c2b42d + 5de9250 commit bae8fdb

File tree

11 files changed

+1586
-340
lines changed

11 files changed

+1586
-340
lines changed

pkg/builder/controller.go

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ import (
2121
"strings"
2222

2323
"github.com/go-logr/logr"
24+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2425
"k8s.io/apimachinery/pkg/runtime/schema"
2526
"k8s.io/client-go/rest"
27+
2628
"sigs.k8s.io/controller-runtime/pkg/client"
2729
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
2830
"sigs.k8s.io/controller-runtime/pkg/controller"
@@ -37,6 +39,17 @@ import (
3739
var newController = controller.New
3840
var getGvk = apiutil.GVKForObject
3941

42+
// project represents other forms that the we can use to
43+
// send/receive a given resource (metadata-only, unstructured, etc)
44+
type objectProjection int
45+
46+
const (
47+
// projectAsNormal doesn't change the object from the form given
48+
projectAsNormal objectProjection = iota
49+
// projectAsMetadata turns this into an metadata-only watch
50+
projectAsMetadata
51+
)
52+
4053
// Builder builds a Controller.
4154
type Builder struct {
4255
forInput ForInput
@@ -57,9 +70,10 @@ func ControllerManagedBy(m manager.Manager) *Builder {
5770

5871
// ForInput represents the information set by For method.
5972
type ForInput struct {
60-
object client.Object
61-
predicates []predicate.Predicate
62-
err error
73+
object client.Object
74+
predicates []predicate.Predicate
75+
objectProjection objectProjection
76+
err error
6377
}
6478

6579
// For defines the type of Object being *reconciled*, and configures the ControllerManagedBy to respond to create / delete /
@@ -82,8 +96,9 @@ func (blder *Builder) For(object client.Object, opts ...ForOption) *Builder {
8296

8397
// OwnsInput represents the information set by Owns method.
8498
type OwnsInput struct {
85-
object client.Object
86-
predicates []predicate.Predicate
99+
object client.Object
100+
predicates []predicate.Predicate
101+
objectProjection objectProjection
87102
}
88103

89104
// Owns defines types of Objects being *generated* by the ControllerManagedBy, and configures the ControllerManagedBy to respond to
@@ -188,19 +203,43 @@ func (blder *Builder) Build(r reconcile.Reconciler) (controller.Controller, erro
188203
return blder.ctrl, nil
189204
}
190205

206+
func (blder *Builder) project(obj client.Object, proj objectProjection) (client.Object, error) {
207+
switch proj {
208+
case projectAsNormal:
209+
return obj, nil
210+
case projectAsMetadata:
211+
metaObj := &metav1.PartialObjectMetadata{}
212+
gvk, err := getGvk(obj, blder.mgr.GetScheme())
213+
if err != nil {
214+
return nil, fmt.Errorf("unable to determine GVK of %T for a metadata-only watch: %w", obj, err)
215+
}
216+
metaObj.SetGroupVersionKind(gvk)
217+
return metaObj, nil
218+
default:
219+
panic(fmt.Sprintf("unexpected projection type %v on type %T, should not be possible since this is an internal field", proj, obj))
220+
}
221+
}
222+
191223
func (blder *Builder) doWatch() error {
192224
// Reconcile type
193-
src := &source.Kind{Type: blder.forInput.object}
225+
typeForSrc, err := blder.project(blder.forInput.object, blder.forInput.objectProjection)
226+
if err != nil {
227+
return err
228+
}
229+
src := &source.Kind{Type: typeForSrc}
194230
hdler := &handler.EnqueueRequestForObject{}
195231
allPredicates := append(blder.globalPredicates, blder.forInput.predicates...)
196-
err := blder.ctrl.Watch(src, hdler, allPredicates...)
197-
if err != nil {
232+
if err := blder.ctrl.Watch(src, hdler, allPredicates...); err != nil {
198233
return err
199234
}
200235

201236
// Watches the managed types
202237
for _, own := range blder.ownsInput {
203-
src := &source.Kind{Type: own.object}
238+
typeForSrc, err := blder.project(own.object, own.objectProjection)
239+
if err != nil {
240+
return err
241+
}
242+
src := &source.Kind{Type: typeForSrc}
204243
hdler := &handler.EnqueueRequestForOwner{
205244
OwnerType: blder.forInput.object,
206245
IsController: true,

pkg/builder/controller_test.go

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,17 @@ import (
2525
"github.com/go-logr/logr"
2626
. "github.com/onsi/ginkgo"
2727
. "github.com/onsi/gomega"
28-
2928
appsv1 "k8s.io/api/apps/v1"
3029
corev1 "k8s.io/api/core/v1"
3130
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3231
"k8s.io/apimachinery/pkg/runtime"
3332
"k8s.io/apimachinery/pkg/runtime/schema"
3433
"k8s.io/apimachinery/pkg/types"
34+
"k8s.io/client-go/rest"
3535
"k8s.io/client-go/util/workqueue"
36+
37+
"sigs.k8s.io/controller-runtime/pkg/cache"
38+
"sigs.k8s.io/controller-runtime/pkg/client"
3639
"sigs.k8s.io/controller-runtime/pkg/controller"
3740
"sigs.k8s.io/controller-runtime/pkg/event"
3841
"sigs.k8s.io/controller-runtime/pkg/handler"
@@ -358,8 +361,60 @@ var _ = Describe("application", func() {
358361
})
359362
})
360363

364+
Describe("watching with projections", func() {
365+
var mgr manager.Manager
366+
BeforeEach(func() {
367+
// use a cache that intercepts requests for fully typed objects to
368+
// ensure we use the projected versions
369+
var err error
370+
mgr, err = manager.New(cfg, manager.Options{NewCache: newNonTypedOnlyCache})
371+
Expect(err).NotTo(HaveOccurred())
372+
})
373+
374+
It("should support watching For & Owns as metadata", func() {
375+
bldr := ControllerManagedBy(mgr).
376+
For(&appsv1.Deployment{}, OnlyMetadata).
377+
Owns(&appsv1.ReplicaSet{}, OnlyMetadata)
378+
379+
ctx, cancel := context.WithCancel(context.Background())
380+
defer cancel()
381+
doReconcileTest(ctx, "8", bldr, mgr, true)
382+
})
383+
})
361384
})
362385

386+
// newNonTypedOnlyCache returns a new cache that wraps the normal cache,
387+
// returning an error if normal, typed objects have informers requested.
388+
func newNonTypedOnlyCache(config *rest.Config, opts cache.Options) (cache.Cache, error) {
389+
normalCache, err := cache.New(config, opts)
390+
if err != nil {
391+
return nil, err
392+
}
393+
return &nonTypedOnlyCache{
394+
Cache: normalCache,
395+
}, nil
396+
}
397+
398+
// nonTypedOnlyCache is a cache.Cache that only provides metadata &
399+
// unstructured informers.
400+
type nonTypedOnlyCache struct {
401+
cache.Cache
402+
}
403+
404+
func (c *nonTypedOnlyCache) GetInformer(ctx context.Context, obj client.Object) (cache.Informer, error) {
405+
switch obj.(type) {
406+
case (*metav1.PartialObjectMetadata):
407+
return c.Cache.GetInformer(ctx, obj)
408+
default:
409+
return nil, fmt.Errorf("did not want to provide an informer for normal type %T", obj)
410+
}
411+
}
412+
func (c *nonTypedOnlyCache) GetInformerForKind(ctx context.Context, gvk schema.GroupVersionKind) (cache.Informer, error) {
413+
return nil, fmt.Errorf("don't try to sidestep the restriction on informer types by calling GetInformerForKind")
414+
}
415+
416+
// TODO(directxman12): this function has too many arguments, and the whole
417+
// "nameSuffix" think is a bit of a hack It should be cleaned up significantly by someone with a bit of time
363418
func doReconcileTest(ctx context.Context, nameSuffix string, blder *Builder, mgr manager.Manager, complete bool) {
364419
deployName := "deploy-name-" + nameSuffix
365420
rsName := "rs-name-" + nameSuffix
@@ -422,8 +477,8 @@ func doReconcileTest(ctx context.Context, nameSuffix string, blder *Builder, mgr
422477
Expect(err).NotTo(HaveOccurred())
423478

424479
By("Waiting for the Deployment Reconcile")
425-
Expect(<-ch).To(Equal(reconcile.Request{
426-
NamespacedName: types.NamespacedName{Namespace: "default", Name: deployName}}))
480+
Eventually(ch).Should(Receive(Equal(reconcile.Request{
481+
NamespacedName: types.NamespacedName{Namespace: "default", Name: deployName}})))
427482

428483
By("Creating a ReplicaSet")
429484
// Expect a Reconcile when an Owned object is managedObjects.
@@ -452,8 +507,8 @@ func doReconcileTest(ctx context.Context, nameSuffix string, blder *Builder, mgr
452507
Expect(err).NotTo(HaveOccurred())
453508

454509
By("Waiting for the ReplicaSet Reconcile")
455-
Expect(<-ch).To(Equal(reconcile.Request{
456-
NamespacedName: types.NamespacedName{Namespace: "default", Name: deployName}}))
510+
Eventually(ch).Should(Receive(Equal(reconcile.Request{
511+
NamespacedName: types.NamespacedName{Namespace: "default", Name: deployName}})))
457512

458513
}
459514

pkg/builder/example_test.go

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,71 @@ import (
2121
"fmt"
2222
"os"
2323

24-
logf "sigs.k8s.io/controller-runtime/pkg/log"
25-
2624
appsv1 "k8s.io/api/apps/v1"
2725
corev1 "k8s.io/api/core/v1"
26+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27+
2828
"sigs.k8s.io/controller-runtime/pkg/builder"
2929
"sigs.k8s.io/controller-runtime/pkg/client"
3030
"sigs.k8s.io/controller-runtime/pkg/client/config"
31+
logf "sigs.k8s.io/controller-runtime/pkg/log"
3132
"sigs.k8s.io/controller-runtime/pkg/log/zap"
3233
"sigs.k8s.io/controller-runtime/pkg/manager"
3334
"sigs.k8s.io/controller-runtime/pkg/manager/signals"
3435
"sigs.k8s.io/controller-runtime/pkg/reconcile"
3536
)
3637

38+
func ExampleBuilder_metadata_only() {
39+
logf.SetLogger(zap.New())
40+
41+
var log = logf.Log.WithName("builder-examples")
42+
43+
mgr, err := manager.New(config.GetConfigOrDie(), manager.Options{})
44+
if err != nil {
45+
log.Error(err, "could not create manager")
46+
os.Exit(1)
47+
}
48+
49+
cl := mgr.GetClient()
50+
err = builder.
51+
ControllerManagedBy(mgr). // Create the ControllerManagedBy
52+
For(&appsv1.ReplicaSet{}). // ReplicaSet is the Application API
53+
Owns(&corev1.Pod{}, builder.OnlyMetadata). // ReplicaSet owns Pods created by it, and caches them as metadata only
54+
Complete(reconcile.Func(func(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
55+
// Read the ReplicaSet
56+
rs := &appsv1.ReplicaSet{}
57+
err := cl.Get(ctx, req.NamespacedName, rs)
58+
if err != nil {
59+
return reconcile.Result{}, client.IgnoreNotFound(err)
60+
}
61+
62+
// List the Pods matching the PodTemplate Labels, but only their metadata
63+
var podsMeta metav1.PartialObjectMetadataList
64+
err = cl.List(ctx, &podsMeta, client.InNamespace(req.Namespace), client.MatchingLabels(rs.Spec.Template.Labels))
65+
if err != nil {
66+
return reconcile.Result{}, client.IgnoreNotFound(err)
67+
}
68+
69+
// Update the ReplicaSet
70+
rs.Labels["pod-count"] = fmt.Sprintf("%v", len(podsMeta.Items))
71+
err = cl.Update(ctx, rs)
72+
if err != nil {
73+
return reconcile.Result{}, err
74+
}
75+
76+
return reconcile.Result{}, nil
77+
}))
78+
if err != nil {
79+
log.Error(err, "could not create controller")
80+
os.Exit(1)
81+
}
82+
83+
if err := mgr.Start(signals.SetupSignalHandler()); err != nil {
84+
log.Error(err, "could not start manager")
85+
os.Exit(1)
86+
}
87+
}
88+
3789
// This example creates a simple application ControllerManagedBy that is configured for ReplicaSets and Pods.
3890
//
3991
// * Create a new application for ReplicaSets that manages Pods owned by the ReplicaSet and calls into

pkg/builder/options.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,36 @@ var _ OwnsOption = &Predicates{}
7676
var _ WatchesOption = &Predicates{}
7777

7878
// }}}
79+
80+
// {{{ For & Owns Dual-Type options
81+
82+
// asProjection configures the projection (currently only metadata) on the input.
83+
// Currently only metadata is supported. We might want to expand
84+
// this to arbitrary non-special local projections in the future.
85+
type projectAs objectProjection
86+
87+
// ApplyToFor applies this configuration to the given ForInput options.
88+
func (p projectAs) ApplyToFor(opts *ForInput) {
89+
opts.objectProjection = objectProjection(p)
90+
}
91+
92+
// ApplyToOwns applies this configuration to the given OwnsInput options.
93+
func (p projectAs) ApplyToOwns(opts *OwnsInput) {
94+
opts.objectProjection = objectProjection(p)
95+
}
96+
97+
var (
98+
// OnlyMetadata tells the controller to *only* cache metadata, and to watch
99+
// the the API server in metadata-only form. This is useful when watching
100+
// lots of objects, really big objects, or objects for which you only know
101+
// the the GVK, but not the structure. You'll need to pass
102+
// metav1.PartialObjectMetadata to the client when fetching objects in your
103+
// reconciler, otherwise you'll end up with a duplicate structured or
104+
// unstructured cache.
105+
OnlyMetadata = projectAs(projectAsMetadata)
106+
107+
_ ForOption = OnlyMetadata
108+
_ OwnsOption = OnlyMetadata
109+
)
110+
111+
// }}}

0 commit comments

Comments
 (0)