Skip to content

Commit afc4e2a

Browse files
committed
Support "projections" in the controller builder
This adds options to "project" watches as only metadata to the builder, making it more convienient to use these forms. For instance: ```go .Owns(&corev1.Pod{}, builder.OnlyMetadata) ``` is equivalent to ```go .Owns(&metav1.PartialObjectMetadata{ TypeMeta: metav1.TypeMeta{ APIVersion: "v1", Kind: "Pod", }, }) ```
1 parent 82d4212 commit afc4e2a

File tree

4 files changed

+192
-16
lines changed

4 files changed

+192
-16
lines changed

pkg/builder/controller.go

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@ 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"
2526
"k8s.io/apimachinery/pkg/runtime/schema"
2627
"k8s.io/client-go/rest"
28+
2729
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
2830
"sigs.k8s.io/controller-runtime/pkg/controller"
2931
"sigs.k8s.io/controller-runtime/pkg/handler"
@@ -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 runtime.Object
61-
predicates []predicate.Predicate
62-
err error
73+
object runtime.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 runtime.Object, opts ...ForOption) *Builder {
8296

8397
// OwnsInput represents the information set by Owns method.
8498
type OwnsInput struct {
85-
object runtime.Object
86-
predicates []predicate.Predicate
99+
object runtime.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
@@ -184,19 +199,43 @@ func (blder *Builder) Build(r reconcile.Reconciler) (controller.Controller, erro
184199
return blder.ctrl, nil
185200
}
186201

202+
func (blder *Builder) project(obj runtime.Object, proj objectProjection) (runtime.Object, error) {
203+
switch proj {
204+
case projectAsNormal:
205+
return obj, nil
206+
case projectAsMetadata:
207+
metaObj := &metav1.PartialObjectMetadata{}
208+
gvk, err := getGvk(obj, blder.mgr.GetScheme())
209+
if err != nil {
210+
return nil, fmt.Errorf("unable to determine GVK of %T for a metadata-only watch: %w", obj, err)
211+
}
212+
metaObj.SetGroupVersionKind(gvk)
213+
return metaObj, nil
214+
default:
215+
panic(fmt.Sprintf("unexpected projection type %v on type %T, should not be possible since this is an internal field", proj, obj))
216+
}
217+
}
218+
187219
func (blder *Builder) doWatch() error {
188220
// Reconcile type
189-
src := &source.Kind{Type: blder.forInput.object}
221+
typeForSrc, err := blder.project(blder.forInput.object, blder.forInput.objectProjection)
222+
if err != nil {
223+
return err
224+
}
225+
src := &source.Kind{Type: typeForSrc}
190226
hdler := &handler.EnqueueRequestForObject{}
191227
allPredicates := append(blder.globalPredicates, blder.forInput.predicates...)
192-
err := blder.ctrl.Watch(src, hdler, allPredicates...)
193-
if err != nil {
228+
if err := blder.ctrl.Watch(src, hdler, allPredicates...); err != nil {
194229
return err
195230
}
196231

197232
// Watches the managed types
198233
for _, own := range blder.ownsInput {
199-
src := &source.Kind{Type: own.object}
234+
typeForSrc, err := blder.project(own.object, own.objectProjection)
235+
if err != nil {
236+
return err
237+
}
238+
src := &source.Kind{Type: typeForSrc}
200239
hdler := &handler.EnqueueRequestForOwner{
201240
OwnerType: blder.forInput.object,
202241
IsController: true,

pkg/builder/controller_test.go

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,16 @@ 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"
3638
"sigs.k8s.io/controller-runtime/pkg/controller"
3739
"sigs.k8s.io/controller-runtime/pkg/event"
3840
"sigs.k8s.io/controller-runtime/pkg/handler"
@@ -345,8 +347,58 @@ var _ = Describe("application", func() {
345347
})
346348
})
347349

350+
Describe("watching with projections", func() {
351+
var mgr manager.Manager
352+
BeforeEach(func() {
353+
// use a cache that intercepts requests for fully typed objects to
354+
// ensure we use the projected versions
355+
var err error
356+
mgr, err = manager.New(cfg, manager.Options{NewCache: newNonTypedOnlyCache})
357+
Expect(err).NotTo(HaveOccurred())
358+
})
359+
360+
It("should support watching For & Owns as metadata", func() {
361+
bldr := ControllerManagedBy(mgr).
362+
For(&appsv1.Deployment{}, OnlyMetadata).
363+
Owns(&appsv1.ReplicaSet{}, OnlyMetadata)
364+
365+
doReconcileTest("8", stop, bldr, mgr, true)
366+
})
367+
})
348368
})
349369

370+
// newNonTypedOnlyCache returns a new cache that wraps the normal cache,
371+
// returning an error if normal, typed objects have informers requested.
372+
func newNonTypedOnlyCache(config *rest.Config, opts cache.Options) (cache.Cache, error) {
373+
normalCache, err := cache.New(config, opts)
374+
if err != nil {
375+
return nil, err
376+
}
377+
return &nonTypedOnlyCache{
378+
Cache: normalCache,
379+
}, nil
380+
}
381+
382+
// nonTypedOnlyCache is a cache.Cache that only provides metadata &
383+
// unstructured informers.
384+
type nonTypedOnlyCache struct {
385+
cache.Cache
386+
}
387+
388+
func (c *nonTypedOnlyCache) GetInformer(ctx context.Context, obj runtime.Object) (cache.Informer, error) {
389+
switch obj.(type) {
390+
case (*metav1.PartialObjectMetadata):
391+
return c.Cache.GetInformer(ctx, obj)
392+
default:
393+
return nil, fmt.Errorf("did not want to provide an informer for normal type %T", obj)
394+
}
395+
}
396+
func (c *nonTypedOnlyCache) GetInformerForKind(ctx context.Context, gvk schema.GroupVersionKind) (cache.Informer, error) {
397+
return nil, fmt.Errorf("don't try to sidestep the restriction on informer types by calling GetInformerForKind")
398+
}
399+
400+
// TODO(directxman12): this function has too many arguments, and the whole
401+
// "nameSuffix" think is a bit of a hack It should be cleaned up significantly by someone with a bit of time
350402
func doReconcileTest(nameSuffix string, stop chan struct{}, blder *Builder, mgr manager.Manager, complete bool) {
351403
deployName := "deploy-name-" + nameSuffix
352404
rsName := "rs-name-" + nameSuffix
@@ -409,8 +461,8 @@ func doReconcileTest(nameSuffix string, stop chan struct{}, blder *Builder, mgr
409461
Expect(err).NotTo(HaveOccurred())
410462

411463
By("Waiting for the Deployment Reconcile")
412-
Expect(<-ch).To(Equal(reconcile.Request{
413-
NamespacedName: types.NamespacedName{Namespace: "default", Name: deployName}}))
464+
Eventually(ch).Should(Receive(Equal(reconcile.Request{
465+
NamespacedName: types.NamespacedName{Namespace: "default", Name: deployName}})))
414466

415467
By("Creating a ReplicaSet")
416468
// Expect a Reconcile when an Owned object is managedObjects.
@@ -439,8 +491,8 @@ func doReconcileTest(nameSuffix string, stop chan struct{}, blder *Builder, mgr
439491
Expect(err).NotTo(HaveOccurred())
440492

441493
By("Waiting for the ReplicaSet Reconcile")
442-
Expect(<-ch).To(Equal(reconcile.Request{
443-
NamespacedName: types.NamespacedName{Namespace: "default", Name: deployName}}))
494+
Eventually(ch).Should(Receive(Equal(reconcile.Request{
495+
NamespacedName: types.NamespacedName{Namespace: "default", Name: deployName}})))
444496

445497
}
446498

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)