Skip to content
This repository has been archived by the owner on Aug 28, 2024. It is now read-only.

Commit

Permalink
Allow ChildReconciler to reflect invalid api errors (#418)
Browse files Browse the repository at this point in the history
API errors with an "invalid" response from the server are now passed to
ReflectChildStatusOnParent. They can be detected with:

    apierrs.IsInvalid(err)

Signed-off-by: Scott Andrews <andrewssc@vmware.com>
  • Loading branch information
scothis authored Aug 31, 2023
1 parent 5be8c31 commit 4c2cc52
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 2 deletions.
10 changes: 9 additions & 1 deletion reconcilers/child.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ type ChildReconciler[Type, ChildType client.Object, ChildListType client.ObjectL
// ReflectChildStatusOnParent updates the reconciled resource's status with values from the
// child. Select types of errors are passed, including:
// - apierrs.IsAlreadyExists
// - apierrs.IsInvalid
//
// Most errors are returned directly, skipping this method. The set of handled error types
// may grow, implementations should be defensive rather than assuming the error type.
Expand Down Expand Up @@ -165,6 +166,9 @@ func (r *ChildReconciler[T, CT, CLT]) init() {
if r.Name == "" {
r.Name = fmt.Sprintf("%sChildReconciler", typeName(r.ChildType))
}
if r.Sanitize == nil {
r.Sanitize = func(child CT) interface{} { return child }
}
if r.stamp == nil {
r.stamp = &ResourceManager[CT]{
Name: r.Name,
Expand Down Expand Up @@ -251,7 +255,8 @@ func (r *ChildReconciler[T, CT, CLT]) Reconcile(ctx context.Context, resource T)
return Result{}, err
}
if err != nil {
if apierrs.IsAlreadyExists(err) {
switch {
case apierrs.IsAlreadyExists(err):
// check if the resource blocking create is owned by the reconciled resource.
// the created child from a previous turn may be slow to appear in the informer cache, but shouldn't appear
// on the reconciled resource as being not ready.
Expand All @@ -265,6 +270,9 @@ func (r *ChildReconciler[T, CT, CLT]) Reconcile(ctx context.Context, resource T)
log.Info("unable to reconcile child, not owned", "child", namespaceName(conflicted), "ownerRefs", conflicted.GetOwnerReferences())
r.ReflectChildStatusOnParent(ctx, resource, child, err)
return Result{}, nil
case apierrs.IsInvalid(err):
r.ReflectChildStatusOnParent(ctx, resource, child, err)
return Result{}, nil
}
log.Error(err, "unable to reconcile child")
return Result{}, err
Expand Down
44 changes: 43 additions & 1 deletion reconcilers/child_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/validation/field"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/utils/pointer"
"sigs.k8s.io/controller-runtime/pkg/client"
Expand Down Expand Up @@ -89,9 +90,13 @@ func TestChildReconciler(t *testing.T) {
},
ReflectChildStatusOnParent: func(ctx context.Context, parent *resources.TestResource, child *corev1.ConfigMap, err error) {
if err != nil {
if apierrs.IsAlreadyExists(err) {
switch {
case apierrs.IsAlreadyExists(err):
name := err.(apierrs.APIStatus).Status().Details.Name
parent.Status.MarkNotReady(ctx, "NameConflict", "%q already exists", name)
case apierrs.IsInvalid(err):
name := err.(apierrs.APIStatus).Status().Details.Name
parent.Status.MarkNotReady(ctx, "InvalidChild", "%q was rejected by the api server", name)
}
return
}
Expand Down Expand Up @@ -657,6 +662,43 @@ func TestChildReconciler(t *testing.T) {
},
},
},
"invalid child": {
Resource: resourceReady.
SpecDie(func(d *dies.TestResourceSpecDie) {
d.AddField("foo", "bar")
}).
DieReleasePtr(),
WithReactors: []rtesting.ReactionFunc{
rtesting.InduceFailure("create", "ConfigMap", rtesting.InduceFailureOpts{
Error: apierrs.NewInvalid(schema.GroupKind{}, testName, field.ErrorList{
field.Invalid(field.NewPath("metadata", "name"), testName, ""),
}),
}),
},
Metadata: map[string]interface{}{
"SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] {
return defaultChildReconciler(c)
},
},
ExpectResource: resourceReady.
SpecDie(func(d *dies.TestResourceSpecDie) {
d.AddField("foo", "bar")
}).
StatusDie(func(d *dies.TestResourceStatusDie) {
d.ConditionsDie(
diemetav1.ConditionBlank.Type(apis.ConditionReady).Status(metav1.ConditionFalse).
Reason("InvalidChild").Message(`"test-resource" was rejected by the api server`),
)
}).
DieReleasePtr(),
ExpectEvents: []rtesting.Event{
rtesting.NewEvent(resource, scheme, corev1.EventTypeWarning, "CreationFailed",
"Failed to create ConfigMap %q: %q is invalid: metadata.name: Invalid value: %q", testName, testName, testName),
},
ExpectCreates: []client.Object{
configMapCreate,
},
},
"child name collision": {
Resource: resourceReady.
SpecDie(func(d *dies.TestResourceSpecDie) {
Expand Down
1 change: 1 addition & 0 deletions reconcilers/childset.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ type ChildSetReconciler[Type, ChildType client.Object, ChildListType client.Obje
// ReflectChildrenStatusOnParent updates the reconciled resource's status with values from the
// child reconciliations. Select types of errors are captured, including:
// - apierrs.IsAlreadyExists
// - apierrs.IsInvalid
//
// Most errors are returned directly, skipping this method. The set of handled error types
// may grow, implementations should be defensive rather than assuming the error type.
Expand Down

0 comments on commit 4c2cc52

Please sign in to comment.