Skip to content

Commit

Permalink
Allow ChildReconciler to manage resource in other namespaces (#236)
Browse files Browse the repository at this point in the history
When listing candidate children, by default we limit the listing to
resources in the same namespace as the parent for efficiency. Now a
ChildReconciler may use the ListOptions method to return a custom set of
[]ListOption. This may use any list options available within controller
runtime, including indexed fields.

When used in conjunction with finalizers and WithConfig we should be
able to manage child resources in other clusters (needs verification).

Signed-off-by: Scott Andrews <andrewssc@vmware.com>
  • Loading branch information
scothis committed May 13, 2022
1 parent eed91e0 commit ed9214d
Show file tree
Hide file tree
Showing 3 changed files with 156 additions and 1 deletion.
43 changes: 42 additions & 1 deletion reconcilers/reconcilers.go
Expand Up @@ -746,6 +746,20 @@ type ChildReconciler struct {
// func(a1, a2 client.Object) bool
SemanticEquals interface{}

// ListOptions allows custom options to be use when listing potential child resources. Each
// resource retrieved as part of the listing is confirmed via OurChild.
//
// Defaults to filtering by the parent's namespace:
// []client.ListOption{
// client.InNamespace(parent.GetNamespace()),
// }
//
// Expected function signature:
// func(ctx context.Context, parent client.Object) []client.ListOption
//
// +optional
ListOptions interface{}

// OurChild is used when there are multiple ChildReconciler for the same ChildType
// controlled by the same parent object. The function return true for child resources
// managed by this ChildReconciler. Objects returned from the DesiredChild function
Expand Down Expand Up @@ -880,6 +894,19 @@ func (r *ChildReconciler) validate(ctx context.Context) error {
}
}

// validate ListOptions function signature:
// nil
// func(ctx context.Context, parent client.Object) []client.ListOption
if r.ListOptions != nil {
fn := reflect.TypeOf(r.ListOptions)
if fn.NumIn() != 2 || fn.NumOut() != 1 ||
!reflect.TypeOf((*context.Context)(nil)).Elem().AssignableTo(fn.In(0)) ||
!reflect.TypeOf(castParentType).AssignableTo(fn.In(1)) ||
!reflect.TypeOf([]client.ListOption{}).AssignableTo(fn.Out(0)) {
return fmt.Errorf("ChildReconciler %q must implement ListOptions: nil | func(context.Context, %s) []client.ListOption, found: %s", r.Name, reflect.TypeOf(castParentType), fn)
}
}

// validate OurChild function signature:
// nil
// func(parent, child client.Object) bool
Expand Down Expand Up @@ -954,7 +981,7 @@ func (r *ChildReconciler) reconcile(ctx context.Context, parent client.Object) (

actual := newEmpty(r.ChildType).(client.Object)
children := newEmpty(r.ChildListType).(client.ObjectList)
if err := c.List(ctx, children, client.InNamespace(parent.GetNamespace())); err != nil {
if err := c.List(ctx, children, r.listOptions(ctx, parent)...); err != nil {
return nil, err
}
items := r.filterChildren(parent, children)
Expand Down Expand Up @@ -1180,6 +1207,20 @@ func (r *ChildReconciler) filterChildren(parent client.Object, children client.O
return items
}

func (r *ChildReconciler) listOptions(ctx context.Context, parent client.Object) []client.ListOption {
if r.ListOptions == nil {
return []client.ListOption{
client.InNamespace(parent.GetNamespace()),
}
}
fn := reflect.ValueOf(r.ListOptions)
out := fn.Call([]reflect.Value{
reflect.ValueOf(ctx),
reflect.ValueOf(parent),
})
return out[0].Interface().([]client.ListOption)
}

func (r *ChildReconciler) ourChild(parent, obj client.Object) bool {
if !metav1.IsControlledBy(obj, parent) {
return false
Expand Down
26 changes: 26 additions & 0 deletions reconcilers/reconcilers_test.go
Expand Up @@ -884,6 +884,32 @@ func TestChildReconciler(t *testing.T) {
return defaultChildReconciler(c)
},
},
}, {
Name: "child is in sync, in a different namespace",
Parent: resourceReady.
SpecDie(func(d *dies.TestResourceSpecDie) {
d.AddField("foo", "bar")
}).
StatusDie(func(d *dies.TestResourceStatusDie) {
d.AddField("foo", "bar")
}),
GivenObjects: []client.Object{
configMapGiven.
MetadataDie(func(d *diemetav1.ObjectMetaDie) {
d.Namespace("other-ns")
}),
},
Metadata: map[string]interface{}{
"SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler {
r := defaultChildReconciler(c)
r.ListOptions = func(ctx context.Context, parent *resources.TestResource) []client.ListOption {
return []client.ListOption{
client.InNamespace("other-ns"),
}
}
return r
},
},
}, {
Name: "create child",
Parent: resource.
Expand Down
88 changes: 88 additions & 0 deletions reconcilers/reconcilers_validate_test.go
Expand Up @@ -755,6 +755,94 @@ func TestChildReconciler_validate(t *testing.T) {
},
shouldErr: `ChildReconciler "HarmonizeImmutableFields num out" must implement HarmonizeImmutableFields: nil | func(*v1.Pod, *v1.Pod), found: func(*v1.Pod, *v1.Pod) error`,
},
{
name: "ListOptions",
parent: &corev1.ConfigMap{},
reconciler: &ChildReconciler{
ChildType: &corev1.Pod{},
ChildListType: &corev1.PodList{},
DesiredChild: func(ctx context.Context, parent *corev1.ConfigMap) (*corev1.Pod, error) { return nil, nil },
ReflectChildStatusOnParent: func(parent *corev1.ConfigMap, child *corev1.Pod, err error) {},
MergeBeforeUpdate: func(current, desired *corev1.Pod) {},
SemanticEquals: func(a1, a2 *corev1.Pod) bool { return false },
ListOptions: func(ctx context.Context, parent *corev1.ConfigMap) []client.ListOption { return []client.ListOption{} },
},
},
{
name: "ListOptions num in",
parent: &corev1.ConfigMap{},
reconciler: &ChildReconciler{
Name: "ListOptions num in",
ChildType: &corev1.Pod{},
ChildListType: &corev1.PodList{},
DesiredChild: func(ctx context.Context, parent *corev1.ConfigMap) (*corev1.Pod, error) { return nil, nil },
ReflectChildStatusOnParent: func(parent *corev1.ConfigMap, child *corev1.Pod, err error) {},
MergeBeforeUpdate: func(current, desired *corev1.Pod) {},
SemanticEquals: func(a1, a2 *corev1.Pod) bool { return false },
ListOptions: func() []client.ListOption { return []client.ListOption{} },
},
shouldErr: `ChildReconciler "ListOptions num in" must implement ListOptions: nil | func(context.Context, *v1.ConfigMap) []client.ListOption, found: func() []client.ListOption`,
},
{
name: "ListOptions in 1",
parent: &corev1.ConfigMap{},
reconciler: &ChildReconciler{
Name: "ListOptions in 1",
ChildType: &corev1.Pod{},
ChildListType: &corev1.PodList{},
DesiredChild: func(ctx context.Context, parent *corev1.ConfigMap) (*corev1.Pod, error) { return nil, nil },
ReflectChildStatusOnParent: func(parent *corev1.ConfigMap, child *corev1.Pod, err error) {},
MergeBeforeUpdate: func(current, desired *corev1.Pod) {},
SemanticEquals: func(a1, a2 *corev1.Pod) bool { return false },
ListOptions: func(child *corev1.Secret, parent *corev1.ConfigMap) []client.ListOption { return []client.ListOption{} },
},
shouldErr: `ChildReconciler "ListOptions in 1" must implement ListOptions: nil | func(context.Context, *v1.ConfigMap) []client.ListOption, found: func(*v1.Secret, *v1.ConfigMap) []client.ListOption`,
},
{
name: "ListOptions in 2",
parent: &corev1.ConfigMap{},
reconciler: &ChildReconciler{
Name: "ListOptions in 2",
ChildType: &corev1.Pod{},
ChildListType: &corev1.PodList{},
DesiredChild: func(ctx context.Context, parent *corev1.ConfigMap) (*corev1.Pod, error) { return nil, nil },
ReflectChildStatusOnParent: func(parent *corev1.ConfigMap, child *corev1.Pod, err error) {},
MergeBeforeUpdate: func(current, desired *corev1.Pod) {},
SemanticEquals: func(a1, a2 *corev1.Pod) bool { return false },
ListOptions: func(ctx context.Context, parent *corev1.Secret) []client.ListOption { return []client.ListOption{} },
},
shouldErr: `ChildReconciler "ListOptions in 2" must implement ListOptions: nil | func(context.Context, *v1.ConfigMap) []client.ListOption, found: func(context.Context, *v1.Secret) []client.ListOption`,
},
{
name: "ListOptions num out",
parent: &corev1.ConfigMap{},
reconciler: &ChildReconciler{
Name: "ListOptions num out",
ChildType: &corev1.Pod{},
ChildListType: &corev1.PodList{},
DesiredChild: func(ctx context.Context, parent *corev1.ConfigMap) (*corev1.Pod, error) { return nil, nil },
ReflectChildStatusOnParent: func(parent *corev1.ConfigMap, child *corev1.Pod, err error) {},
MergeBeforeUpdate: func(current, desired *corev1.Pod) {},
SemanticEquals: func(a1, a2 *corev1.Pod) bool { return false },
ListOptions: func(ctx context.Context, parent *corev1.ConfigMap) {},
},
shouldErr: `ChildReconciler "ListOptions num out" must implement ListOptions: nil | func(context.Context, *v1.ConfigMap) []client.ListOption, found: func(context.Context, *v1.ConfigMap)`,
},
{
name: "ListOptions out 1",
parent: &corev1.ConfigMap{},
reconciler: &ChildReconciler{
Name: "ListOptions out 1",
ChildType: &corev1.Pod{},
ChildListType: &corev1.PodList{},
DesiredChild: func(ctx context.Context, parent *corev1.ConfigMap) (*corev1.Pod, error) { return nil, nil },
ReflectChildStatusOnParent: func(parent *corev1.ConfigMap, child *corev1.Pod, err error) {},
MergeBeforeUpdate: func(current, desired *corev1.Pod) {},
SemanticEquals: func(a1, a2 *corev1.Pod) bool { return false },
ListOptions: func(ctx context.Context, parent *corev1.ConfigMap) client.ListOptions { return client.ListOptions{} },
},
shouldErr: `ChildReconciler "ListOptions out 1" must implement ListOptions: nil | func(context.Context, *v1.ConfigMap) []client.ListOption, found: func(context.Context, *v1.ConfigMap) client.ListOptions`,
},
{
name: "OurChild",
parent: &corev1.ConfigMap{},
Expand Down

0 comments on commit ed9214d

Please sign in to comment.