Skip to content

Commit 762bea7

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 f89d3f0 commit 762bea7

File tree

4 files changed

+195
-16
lines changed

4 files changed

+195
-16
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)