Skip to content

Commit 3a67a51

Browse files
committed
Implement generate-apply-based utilities
For implementing generate-apply based controllers, it's often important to know the objects that are owned by a controller. For doing so, the utilities provided with this change allow to * Filter owned objects * List and filter * List and filter owned objects * Get objects from * Slices * Lists * Set objects to * Slices * Lists And, most prominently, the new beta-grade tool called `CreateOrUse` that tries to use or create an object depending on a predefined list.
1 parent 6f8daf5 commit 3a67a51

File tree

4 files changed

+773
-0
lines changed

4 files changed

+773
-0
lines changed

clientutils/clientutils.go

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@ import (
2121
"fmt"
2222
"reflect"
2323

24+
"github.com/onmetal/controller-utils/metautils"
2425
"github.com/onmetal/controller-utils/unstructuredutils"
2526
apierrors "k8s.io/apimachinery/pkg/api/errors"
2627
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
28+
"k8s.io/apimachinery/pkg/conversion"
2729
"k8s.io/apimachinery/pkg/runtime/schema"
2830
"sigs.k8s.io/controller-runtime/pkg/client"
2931
)
@@ -384,3 +386,162 @@ func DeleteMultiple(ctx context.Context, c client.Client, objs []client.Object,
384386
}
385387
return nil
386388
}
389+
390+
// ListAndFilter is a shorthand for doing a client.Client.List followed by filtering the list's elements
391+
// with the given function.
392+
func ListAndFilter(ctx context.Context, c client.Client, list client.ObjectList, filterFunc func(object client.Object) (bool, error), opts ...client.ListOption) error {
393+
if err := c.List(ctx, list, opts...); err != nil {
394+
return err
395+
}
396+
397+
items, err := metautils.ExtractList(list)
398+
if err != nil {
399+
return fmt.Errorf("error extracting list: %w", err)
400+
}
401+
402+
var filtered []client.Object
403+
for _, item := range items {
404+
ok, err := filterFunc(item)
405+
if err != nil {
406+
return err
407+
}
408+
if ok {
409+
item := item
410+
filtered = append(filtered, item)
411+
}
412+
}
413+
414+
if err := metautils.SetList(list, filtered); err != nil {
415+
return fmt.Errorf("error setting list: %w", err)
416+
}
417+
418+
return nil
419+
}
420+
421+
// ListAndFilterControlledBy is a shorthand for doing a client.List followed by filtering the list's elements
422+
// using metautils.IsControlledBy.
423+
func ListAndFilterControlledBy(ctx context.Context, c client.Client, owner client.Object, list client.ObjectList, opts ...client.ListOption) error {
424+
scheme := c.Scheme()
425+
return ListAndFilter(ctx, c, list, func(object client.Object) (bool, error) {
426+
return metautils.IsControlledBy(scheme, owner, object)
427+
}, opts...)
428+
}
429+
430+
func setObject(dst, src client.Object) error {
431+
dstV, err := conversion.EnforcePtr(dst)
432+
if err != nil {
433+
return err
434+
}
435+
436+
srcV, err := conversion.EnforcePtr(src)
437+
if err != nil {
438+
return err
439+
}
440+
441+
if !srcV.Type().AssignableTo(dstV.Type()) {
442+
return fmt.Errorf("cannot assign %T to %T", src, dst)
443+
}
444+
445+
dstV.Set(srcV.Convert(dstV.Type()))
446+
return nil
447+
}
448+
449+
// CreateOrUseOperationResult is the result of a CreateOrUse call.
450+
type CreateOrUseOperationResult string
451+
452+
const (
453+
// CreateOrUseOperationResultUsed indicates that a match has been found and no object was created.
454+
CreateOrUseOperationResultUsed CreateOrUseOperationResult = "used"
455+
// CreateOrUseOperationResultCreated indicates that no match has been found and a new object was created.
456+
CreateOrUseOperationResultCreated CreateOrUseOperationResult = "created"
457+
// CreateOrUseOperationResultNone indicates that no match has been found / no object was created.
458+
CreateOrUseOperationResultNone CreateOrUseOperationResult = "unchanged"
459+
)
460+
461+
// CreateOrUse traverses through a slice of objects and tries to find a matching object using matchFunc.
462+
// If it does, the matching object is set to the object and the function returns the other objects.
463+
// If it matches multiple times, the winning object is the oldest.
464+
// If it does not match, initFunc is called and the new object is created.
465+
func CreateOrUse(ctx context.Context, c client.Client, objects []client.Object, obj client.Object, matchFunc func() (bool, error), initFunc func() error) (CreateOrUseOperationResult, []client.Object, error) {
466+
var (
467+
base = obj.DeepCopyObject().(client.Object)
468+
best client.Object
469+
other []client.Object
470+
)
471+
for _, object := range objects {
472+
object := object
473+
if err := setObject(obj, object); err != nil {
474+
return CreateOrUseOperationResultNone, nil, err
475+
}
476+
477+
match, err := matchFunc()
478+
if err != nil {
479+
return CreateOrUseOperationResultNone, nil, err
480+
}
481+
482+
switch {
483+
case match && best == nil:
484+
best = object
485+
// The older object is better.
486+
// Using older objects over newer objects helps avoid creating too many new objects.
487+
case match && best.GetCreationTimestamp().Time.After(object.GetCreationTimestamp().Time):
488+
other = append(other, best)
489+
best = object
490+
default:
491+
other = append(other, object)
492+
}
493+
}
494+
if best != nil {
495+
if err := setObject(obj, best); err != nil {
496+
return CreateOrUseOperationResultNone, nil, err
497+
}
498+
return CreateOrUseOperationResultUsed, other, nil
499+
}
500+
501+
if err := setObject(obj, base); err != nil {
502+
return CreateOrUseOperationResultNone, nil, err
503+
}
504+
if initFunc != nil {
505+
if err := initFunc(); err != nil {
506+
return CreateOrUseOperationResultNone, nil, err
507+
}
508+
}
509+
if err := c.Create(ctx, obj); err != nil {
510+
return CreateOrUseOperationResultNone, nil, err
511+
}
512+
return CreateOrUseOperationResultCreated, other, nil
513+
}
514+
515+
// CreateOrUseWithList is a shorthand for using CreateOrUse with a client.ObjectList containing the objects.
516+
// Caution: In contrast to CreateOrUse, if setting the list elements fails, the unmatched objects are unknown.
517+
// The operation result will still reflect what happened.
518+
func CreateOrUseWithList(ctx context.Context, c client.Client, list client.ObjectList, obj client.Object, matchFunc func() (bool, error), initFunc func() error) (CreateOrUseOperationResult, error) {
519+
items, err := metautils.ExtractList(list)
520+
if err != nil {
521+
return CreateOrUseOperationResultNone, err
522+
}
523+
524+
res, items, err := CreateOrUse(ctx, c, items, obj, matchFunc, initFunc)
525+
if err != nil {
526+
return res, err
527+
}
528+
529+
return res, metautils.SetList(list, items)
530+
}
531+
532+
// CreateOrUseWithObjectSlicePointer is a shorthand for using CreateOrUse with an object slice containing the objects.
533+
// Caution: In contrast to CreateOrUse, if setting the slice elements fails, the unmatched objects are unknown.
534+
// The operation result will still reflect what happened.
535+
func CreateOrUseWithObjectSlicePointer(ctx context.Context, c client.Client, slicePtr interface{}, obj client.Object, matchFunc func() (bool, error), initFunc func() error) (CreateOrUseOperationResult, error) {
536+
items, err := metautils.ExtractObjectSlicePointer(slicePtr)
537+
if err != nil {
538+
return CreateOrUseOperationResultNone, err
539+
}
540+
541+
res, items, err := CreateOrUse(ctx, c, items, obj, matchFunc, initFunc)
542+
if err != nil {
543+
return res, err
544+
}
545+
546+
return res, metautils.SetObjectSlice(slicePtr, items)
547+
}

0 commit comments

Comments
 (0)