Skip to content

Commit dbc25a8

Browse files
ncdcDirectXMan12
andcommitted
Support metadata-only watches
Add support for metadata-only watches. This backports a series of commits from the main branch: Add low-level metadata-only informer support This adds support for informers that communicate with the API server in metadata-only form. They are *completely* separate from normal informers -- that is: just like unstructured, if you ask for both a "normal" informer & a metadata-only informer, you'll get two copies of the cache. Support metadata-only client operations This adds support for a metadata-only client. It only implements the operations supported by metadata (delete, deleteallof, patch, get, list, status.patch). The higher-level client will now delegate to this for when a PartialObjectMetadata object is passed in. 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", }, }) ``` Co-authored-by: Solly Ross <[email protected]>
1 parent ac380d6 commit dbc25a8

File tree

11 files changed

+1577
-337
lines changed

11 files changed

+1577
-337
lines changed

pkg/builder/controller.go

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ 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"
@@ -37,6 +38,17 @@ import (
3738
var newController = controller.New
3839
var getGvk = apiutil.GVKForObject
3940

41+
// project represents other forms that the we can use to
42+
// send/receive a given resource (metadata-only, unstructured, etc)
43+
type objectProjection int
44+
45+
const (
46+
// projectAsNormal doesn't change the object from the form given
47+
projectAsNormal objectProjection = iota
48+
// projectAsMetadata turns this into an metadata-only watch
49+
projectAsMetadata
50+
)
51+
4052
// Builder builds a Controller.
4153
type Builder struct {
4254
forInput ForInput
@@ -68,8 +80,9 @@ func (blder *Builder) ForType(apiType runtime.Object) *Builder {
6880

6981
// ForInput represents the information set by For method.
7082
type ForInput struct {
71-
object runtime.Object
72-
predicates []predicate.Predicate
83+
object runtime.Object
84+
predicates []predicate.Predicate
85+
objectProjection objectProjection
7386
}
7487

7588
// For defines the type of Object being *reconciled*, and configures the ControllerManagedBy to respond to create / delete /
@@ -88,8 +101,9 @@ func (blder *Builder) For(object runtime.Object, opts ...ForOption) *Builder {
88101

89102
// OwnsInput represents the information set by Owns method.
90103
type OwnsInput struct {
91-
object runtime.Object
92-
predicates []predicate.Predicate
104+
object runtime.Object
105+
predicates []predicate.Predicate
106+
objectProjection objectProjection
93107
}
94108

95109
// Owns defines types of Objects being *generated* by the ControllerManagedBy, and configures the ControllerManagedBy to respond to
@@ -195,19 +209,43 @@ func (blder *Builder) Build(r reconcile.Reconciler) (controller.Controller, erro
195209
return blder.ctrl, nil
196210
}
197211

212+
func (blder *Builder) project(obj runtime.Object, proj objectProjection) (runtime.Object, error) {
213+
switch proj {
214+
case projectAsNormal:
215+
return obj, nil
216+
case projectAsMetadata:
217+
metaObj := &metav1.PartialObjectMetadata{}
218+
gvk, err := getGvk(obj, blder.mgr.GetScheme())
219+
if err != nil {
220+
return nil, fmt.Errorf("unable to determine GVK of %T for a metadata-only watch: %w", obj, err)
221+
}
222+
metaObj.SetGroupVersionKind(gvk)
223+
return metaObj, nil
224+
default:
225+
panic(fmt.Sprintf("unexpected projection type %v on type %T, should not be possible since this is an internal field", proj, obj))
226+
}
227+
}
228+
198229
func (blder *Builder) doWatch() error {
199230
// Reconcile type
200-
src := &source.Kind{Type: blder.forInput.object}
231+
typeForSrc, err := blder.project(blder.forInput.object, blder.forInput.objectProjection)
232+
if err != nil {
233+
return err
234+
}
235+
src := &source.Kind{Type: typeForSrc}
201236
hdler := &handler.EnqueueRequestForObject{}
202237
allPredicates := append(blder.globalPredicates, blder.forInput.predicates...)
203-
err := blder.ctrl.Watch(src, hdler, allPredicates...)
204-
if err != nil {
238+
if err := blder.ctrl.Watch(src, hdler, allPredicates...); err != nil {
205239
return err
206240
}
207241

208242
// Watches the managed types
209243
for _, own := range blder.ownsInput {
210-
src := &source.Kind{Type: own.object}
244+
typeForSrc, err := blder.project(own.object, own.objectProjection)
245+
if err != nil {
246+
return err
247+
}
248+
src := &source.Kind{Type: typeForSrc}
211249
hdler := &handler.EnqueueRequestForOwner{
212250
OwnerType: blder.forInput.object,
213251
IsController: true,

pkg/builder/controller_test.go

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import (
2424

2525
. "github.com/onsi/ginkgo"
2626
. "github.com/onsi/gomega"
27+
"k8s.io/client-go/rest"
28+
"sigs.k8s.io/controller-runtime/pkg/cache"
2729

2830
appsv1 "k8s.io/api/apps/v1"
2931
corev1 "k8s.io/api/core/v1"
@@ -294,8 +296,58 @@ var _ = Describe("application", func() {
294296
})
295297
})
296298

299+
Describe("watching with projections", func() {
300+
var mgr manager.Manager
301+
BeforeEach(func() {
302+
// use a cache that intercepts requests for fully typed objects to
303+
// ensure we use the projected versions
304+
var err error
305+
mgr, err = manager.New(cfg, manager.Options{NewCache: newNonTypedOnlyCache})
306+
Expect(err).NotTo(HaveOccurred())
307+
})
308+
309+
It("should support watching For & Owns as metadata", func() {
310+
bldr := ControllerManagedBy(mgr).
311+
For(&appsv1.Deployment{}, OnlyMetadata).
312+
Owns(&appsv1.ReplicaSet{}, OnlyMetadata)
313+
314+
doReconcileTest("8", stop, bldr, mgr, true)
315+
})
316+
})
297317
})
298318

319+
// newNonTypedOnlyCache returns a new cache that wraps the normal cache,
320+
// returning an error if normal, typed objects have informers requested.
321+
func newNonTypedOnlyCache(config *rest.Config, opts cache.Options) (cache.Cache, error) {
322+
normalCache, err := cache.New(config, opts)
323+
if err != nil {
324+
return nil, err
325+
}
326+
return &nonTypedOnlyCache{
327+
Cache: normalCache,
328+
}, nil
329+
}
330+
331+
// nonTypedOnlyCache is a cache.Cache that only provides metadata &
332+
// unstructured informers.
333+
type nonTypedOnlyCache struct {
334+
cache.Cache
335+
}
336+
337+
func (c *nonTypedOnlyCache) GetInformer(ctx context.Context, obj runtime.Object) (cache.Informer, error) {
338+
switch obj.(type) {
339+
case (*metav1.PartialObjectMetadata):
340+
return c.Cache.GetInformer(ctx, obj)
341+
default:
342+
return nil, fmt.Errorf("did not want to provide an informer for normal type %T", obj)
343+
}
344+
}
345+
func (c *nonTypedOnlyCache) GetInformerForKind(ctx context.Context, gvk schema.GroupVersionKind) (cache.Informer, error) {
346+
return nil, fmt.Errorf("don't try to sidestep the restriction on informer types by calling GetInformerForKind")
347+
}
348+
349+
// TODO(directxman12): this function has too many arguments, and the whole
350+
// "nameSuffix" think is a bit of a hack It should be cleaned up significantly by someone with a bit of time
299351
func doReconcileTest(nameSuffix string, stop chan struct{}, blder *Builder, mgr manager.Manager, complete bool) {
300352
deployName := "deploy-name-" + nameSuffix
301353
rsName := "rs-name-" + nameSuffix
@@ -358,8 +410,8 @@ func doReconcileTest(nameSuffix string, stop chan struct{}, blder *Builder, mgr
358410
Expect(err).NotTo(HaveOccurred())
359411

360412
By("Waiting for the Deployment Reconcile")
361-
Expect(<-ch).To(Equal(reconcile.Request{
362-
NamespacedName: types.NamespacedName{Namespace: "default", Name: deployName}}))
413+
Eventually(ch).Should(Receive(Equal(reconcile.Request{
414+
NamespacedName: types.NamespacedName{Namespace: "default", Name: deployName}})))
363415

364416
By("Creating a ReplicaSet")
365417
// Expect a Reconcile when an Owned object is managedObjects.
@@ -388,8 +440,8 @@ func doReconcileTest(nameSuffix string, stop chan struct{}, blder *Builder, mgr
388440
Expect(err).NotTo(HaveOccurred())
389441

390442
By("Waiting for the ReplicaSet Reconcile")
391-
Expect(<-ch).To(Equal(reconcile.Request{
392-
NamespacedName: types.NamespacedName{Namespace: "default", Name: deployName}}))
443+
Eventually(ch).Should(Receive(Equal(reconcile.Request{
444+
NamespacedName: types.NamespacedName{Namespace: "default", Name: deployName}})))
393445

394446
}
395447

pkg/builder/example_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"fmt"
2222
"os"
2323

24+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2425
logf "sigs.k8s.io/controller-runtime/pkg/log"
2526

2627
appsv1 "k8s.io/api/apps/v1"
@@ -34,6 +35,60 @@ import (
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(req reconcile.Request) (reconcile.Result, error) {
55+
ctx, cancel := context.WithCancel(context.Background())
56+
defer cancel()
57+
58+
// Read the ReplicaSet
59+
rs := &appsv1.ReplicaSet{}
60+
err := cl.Get(ctx, req.NamespacedName, rs)
61+
if err != nil {
62+
return reconcile.Result{}, client.IgnoreNotFound(err)
63+
}
64+
65+
// List the Pods matching the PodTemplate Labels, but only their metadata
66+
var podsMeta metav1.PartialObjectMetadataList
67+
err = cl.List(ctx, &podsMeta, client.InNamespace(req.Namespace), client.MatchingLabels(rs.Spec.Template.Labels))
68+
if err != nil {
69+
return reconcile.Result{}, client.IgnoreNotFound(err)
70+
}
71+
72+
// Update the ReplicaSet
73+
rs.Labels["pod-count"] = fmt.Sprintf("%v", len(podsMeta.Items))
74+
err = cl.Update(ctx, rs)
75+
if err != nil {
76+
return reconcile.Result{}, err
77+
}
78+
79+
return reconcile.Result{}, nil
80+
}))
81+
if err != nil {
82+
log.Error(err, "could not create controller")
83+
os.Exit(1)
84+
}
85+
86+
if err := mgr.Start(signals.SetupSignalHandler()); err != nil {
87+
log.Error(err, "could not start manager")
88+
os.Exit(1)
89+
}
90+
}
91+
3792
// This example creates a simple application ControllerManagedBy that is configured for ReplicaSets and Pods.
3893
//
3994
// * 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)