diff --git a/reconcilers/reconcilers.go b/reconcilers/reconcilers.go index 3e38dd8..2036162 100644 --- a/reconcilers/reconcilers.go +++ b/reconcilers/reconcilers.go @@ -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 @@ -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 @@ -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) @@ -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 diff --git a/reconcilers/reconcilers_test.go b/reconcilers/reconcilers_test.go index 64c8a63..01b6539 100644 --- a/reconcilers/reconcilers_test.go +++ b/reconcilers/reconcilers_test.go @@ -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. diff --git a/reconcilers/reconcilers_validate_test.go b/reconcilers/reconcilers_validate_test.go index cc3c9d5..62cd715 100644 --- a/reconcilers/reconcilers_validate_test.go +++ b/reconcilers/reconcilers_validate_test.go @@ -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{},