Merge pull request #288 from crossplane/backport-283-to-release-0.15
[Backport release-0.15] Account for two different kinds of consistency issues
This commit is contained in:
commit
5141d0bb35
114
pkg/meta/meta.go
114
pkg/meta/meta.go
|
|
@ -21,6 +21,7 @@ import (
|
|||
"fmt"
|
||||
"hash/fnv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
|
@ -31,9 +32,32 @@ import (
|
|||
xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
|
||||
)
|
||||
|
||||
// AnnotationKeyExternalName is the key in the annotations map of a resource for
|
||||
// the name of the resource as it appears on provider's systems.
|
||||
const AnnotationKeyExternalName = "crossplane.io/external-name"
|
||||
const (
|
||||
// AnnotationKeyExternalName is the key in the annotations map of a
|
||||
// resource for the name of the resource as it appears on provider's
|
||||
// systems.
|
||||
AnnotationKeyExternalName = "crossplane.io/external-name"
|
||||
|
||||
// AnnotationKeyExternalCreatePending is the key in the annotations map
|
||||
// of a resource that indicates the last time creation of the external
|
||||
// resource was pending (i.e. about to happen). Its value must be an
|
||||
// RFC3999 timestamp.
|
||||
AnnotationKeyExternalCreatePending = "crossplane.io/external-create-pending"
|
||||
|
||||
// AnnotationKeyExternalCreateSucceeded is the key in the annotations
|
||||
// map of a resource that represents the last time the external resource
|
||||
// was created successfully. Its value must be an RFC3339 timestamp,
|
||||
// which can be used to determine how long ago a resource was created.
|
||||
// This is useful for eventually consistent APIs that may take some time
|
||||
// before the API called by Observe will report that a recently created
|
||||
// external resource exists.
|
||||
AnnotationKeyExternalCreateSucceeded = "crossplane.io/external-create-succeeded"
|
||||
|
||||
// AnnotationKeyExternalCreateFailed is the key in the annotations map
|
||||
// of a resource that indicates the last time creation of the external
|
||||
// resource failed. Its value must be an RFC3999 timestamp.
|
||||
AnnotationKeyExternalCreateFailed = "crossplane.io/external-create-failed"
|
||||
)
|
||||
|
||||
// Supported resources with all of these annotations will be fully or partially
|
||||
// propagated to the named resource of the same kind, assuming it exists and
|
||||
|
|
@ -245,6 +269,90 @@ func SetExternalName(o metav1.Object, name string) {
|
|||
AddAnnotations(o, map[string]string{AnnotationKeyExternalName: name})
|
||||
}
|
||||
|
||||
// GetExternalCreatePending returns the time at which the external resource
|
||||
// was most recently pending creation.
|
||||
func GetExternalCreatePending(o metav1.Object) time.Time {
|
||||
a := o.GetAnnotations()[AnnotationKeyExternalCreatePending]
|
||||
t, err := time.Parse(time.RFC3339, a)
|
||||
if err != nil {
|
||||
return time.Time{}
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// SetExternalCreatePending sets the time at which the external resource was
|
||||
// most recently pending creation to the supplied time.
|
||||
func SetExternalCreatePending(o metav1.Object, t time.Time) {
|
||||
AddAnnotations(o, map[string]string{AnnotationKeyExternalCreatePending: t.Format(time.RFC3339)})
|
||||
}
|
||||
|
||||
// GetExternalCreateSucceeded returns the time at which the external resource
|
||||
// was most recently created.
|
||||
func GetExternalCreateSucceeded(o metav1.Object) time.Time {
|
||||
a := o.GetAnnotations()[AnnotationKeyExternalCreateSucceeded]
|
||||
t, err := time.Parse(time.RFC3339, a)
|
||||
if err != nil {
|
||||
return time.Time{}
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// SetExternalCreateSucceeded sets the time at which the external resource was
|
||||
// most recently created to the supplied time.
|
||||
func SetExternalCreateSucceeded(o metav1.Object, t time.Time) {
|
||||
AddAnnotations(o, map[string]string{AnnotationKeyExternalCreateSucceeded: t.Format(time.RFC3339)})
|
||||
}
|
||||
|
||||
// GetExternalCreateFailed returns the time at which the external resource
|
||||
// recently failed to create.
|
||||
func GetExternalCreateFailed(o metav1.Object) time.Time {
|
||||
a := o.GetAnnotations()[AnnotationKeyExternalCreateFailed]
|
||||
t, err := time.Parse(time.RFC3339, a)
|
||||
if err != nil {
|
||||
return time.Time{}
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// SetExternalCreateFailed sets the time at which the external resource most
|
||||
// recently failed to create.
|
||||
func SetExternalCreateFailed(o metav1.Object, t time.Time) {
|
||||
AddAnnotations(o, map[string]string{AnnotationKeyExternalCreateFailed: t.Format(time.RFC3339)})
|
||||
}
|
||||
|
||||
// ExternalCreateIncomplete returns true if creation of the external resource
|
||||
// appears to be incomplete. We deem creation to be incomplete if the 'external
|
||||
// create pending' annotation is the newest of all tracking annotations that are
|
||||
// set (i.e. pending, succeeded, and failed).
|
||||
func ExternalCreateIncomplete(o metav1.Object) bool {
|
||||
pending := GetExternalCreatePending(o)
|
||||
succeeded := GetExternalCreateSucceeded(o)
|
||||
failed := GetExternalCreateFailed(o)
|
||||
|
||||
// If creation never started it can't be incomplete.
|
||||
if pending.IsZero() {
|
||||
return false
|
||||
}
|
||||
|
||||
latest := succeeded
|
||||
if failed.After(succeeded) {
|
||||
latest = failed
|
||||
}
|
||||
|
||||
return pending.After(latest)
|
||||
}
|
||||
|
||||
// ExternalCreateSucceededDuring returns true if creation of the external
|
||||
// resource that corresponds to the supplied managed resource succeeded within
|
||||
// the supplied duration.
|
||||
func ExternalCreateSucceededDuring(o metav1.Object, d time.Duration) bool {
|
||||
t := GetExternalCreateSucceeded(o)
|
||||
if t.IsZero() {
|
||||
return false
|
||||
}
|
||||
return time.Since(t) < d
|
||||
}
|
||||
|
||||
// AllowPropagation from one object to another by adding consenting annotations
|
||||
// to both.
|
||||
// Deprecated: This functionality will be removed soon.
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import (
|
|||
"fmt"
|
||||
"hash/fnv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/pkg/errors"
|
||||
|
|
@ -901,6 +902,284 @@ func TestSetExternalName(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestGetExternalCreatePending(t *testing.T) {
|
||||
now := time.Now().Round(time.Second)
|
||||
|
||||
cases := map[string]struct {
|
||||
o metav1.Object
|
||||
want time.Time
|
||||
}{
|
||||
"ExternalCreatePendingExists": {
|
||||
o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{AnnotationKeyExternalCreatePending: now.Format(time.RFC3339)}}},
|
||||
want: now,
|
||||
},
|
||||
"NoExternalCreatePending": {
|
||||
o: &corev1.Pod{},
|
||||
want: time.Time{},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got := GetExternalCreatePending(tc.o)
|
||||
if diff := cmp.Diff(tc.want, got); diff != "" {
|
||||
t.Errorf("GetExternalCreatePending(...): -want, +got:\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetExternalCreatePending(t *testing.T) {
|
||||
now := time.Now()
|
||||
|
||||
cases := map[string]struct {
|
||||
o metav1.Object
|
||||
t time.Time
|
||||
want metav1.Object
|
||||
}{
|
||||
"SetsTheCorrectKey": {
|
||||
o: &corev1.Pod{},
|
||||
t: now,
|
||||
want: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{AnnotationKeyExternalCreatePending: now.Format(time.RFC3339)}}},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
SetExternalCreatePending(tc.o, tc.t)
|
||||
if diff := cmp.Diff(tc.want, tc.o); diff != "" {
|
||||
t.Errorf("SetExternalCreatePending(...): -want, +got:\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetExternalCreateSucceeded(t *testing.T) {
|
||||
now := time.Now().Round(time.Second)
|
||||
|
||||
cases := map[string]struct {
|
||||
o metav1.Object
|
||||
want time.Time
|
||||
}{
|
||||
"ExternalCreateTimeExists": {
|
||||
o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{AnnotationKeyExternalCreateSucceeded: now.Format(time.RFC3339)}}},
|
||||
want: now,
|
||||
},
|
||||
"NoExternalCreateTime": {
|
||||
o: &corev1.Pod{},
|
||||
want: time.Time{},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got := GetExternalCreateSucceeded(tc.o)
|
||||
if diff := cmp.Diff(tc.want, got); diff != "" {
|
||||
t.Errorf("GetExternalCreateSucceeded(...): -want, +got:\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetExternalCreateSucceeded(t *testing.T) {
|
||||
now := time.Now()
|
||||
|
||||
cases := map[string]struct {
|
||||
o metav1.Object
|
||||
t time.Time
|
||||
want metav1.Object
|
||||
}{
|
||||
"SetsTheCorrectKey": {
|
||||
o: &corev1.Pod{},
|
||||
t: now,
|
||||
want: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{AnnotationKeyExternalCreateSucceeded: now.Format(time.RFC3339)}}},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
SetExternalCreateSucceeded(tc.o, tc.t)
|
||||
if diff := cmp.Diff(tc.want, tc.o); diff != "" {
|
||||
t.Errorf("SetExternalCreateSucceeded(...): -want, +got:\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetExternalCreateFailed(t *testing.T) {
|
||||
now := time.Now().Round(time.Second)
|
||||
|
||||
cases := map[string]struct {
|
||||
o metav1.Object
|
||||
want time.Time
|
||||
}{
|
||||
"ExternalCreateFailedExists": {
|
||||
o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{AnnotationKeyExternalCreateFailed: now.Format(time.RFC3339)}}},
|
||||
want: now,
|
||||
},
|
||||
"NoExternalCreateFailed": {
|
||||
o: &corev1.Pod{},
|
||||
want: time.Time{},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got := GetExternalCreateFailed(tc.o)
|
||||
if diff := cmp.Diff(tc.want, got); diff != "" {
|
||||
t.Errorf("GetExternalCreateFailed(...): -want, +got:\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetExternalCreateFailed(t *testing.T) {
|
||||
now := time.Now()
|
||||
|
||||
cases := map[string]struct {
|
||||
o metav1.Object
|
||||
t time.Time
|
||||
want metav1.Object
|
||||
}{
|
||||
"SetsTheCorrectKey": {
|
||||
o: &corev1.Pod{},
|
||||
t: now,
|
||||
want: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{AnnotationKeyExternalCreateFailed: now.Format(time.RFC3339)}}},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
SetExternalCreateFailed(tc.o, tc.t)
|
||||
if diff := cmp.Diff(tc.want, tc.o); diff != "" {
|
||||
t.Errorf("SetExternalCreateFailed(...): -want, +got:\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExternalCreateSucceededDuring(t *testing.T) {
|
||||
type args struct {
|
||||
o metav1.Object
|
||||
d time.Duration
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
"NotYetSuccessfullyCreated": {
|
||||
args: args{
|
||||
o: &corev1.Pod{},
|
||||
d: 1 * time.Minute,
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
"SuccessfullyCreatedTooLongAgo": {
|
||||
args: args{
|
||||
o: func() metav1.Object {
|
||||
o := &corev1.Pod{}
|
||||
t := time.Now().Add(-2 * time.Minute)
|
||||
SetExternalCreateSucceeded(o, t)
|
||||
return o
|
||||
}(),
|
||||
d: 1 * time.Minute,
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
"SuccessfullyCreatedWithinDuration": {
|
||||
args: args{
|
||||
o: func() metav1.Object {
|
||||
o := &corev1.Pod{}
|
||||
t := time.Now().Add(-30 * time.Second)
|
||||
SetExternalCreateSucceeded(o, t)
|
||||
return o
|
||||
}(),
|
||||
d: 1 * time.Minute,
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got := ExternalCreateSucceededDuring(tc.args.o, tc.args.d)
|
||||
if diff := cmp.Diff(tc.want, got); diff != "" {
|
||||
t.Errorf("ExternalCreateSucceededDuring(...): -want, +got:\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExternalCreateIncomplete(t *testing.T) {
|
||||
|
||||
now := time.Now().Format(time.RFC3339)
|
||||
earlier := time.Now().Add(-1 * time.Second).Format(time.RFC3339)
|
||||
evenEarlier := time.Now().Add(-1 * time.Minute).Format(time.RFC3339)
|
||||
|
||||
cases := map[string]struct {
|
||||
reason string
|
||||
o metav1.Object
|
||||
want bool
|
||||
}{
|
||||
"CreateNeverPending": {
|
||||
reason: "If we've never called Create it can't be incomplete.",
|
||||
o: &corev1.Pod{},
|
||||
want: false,
|
||||
},
|
||||
"CreateSucceeded": {
|
||||
reason: "If Create succeeded since it was pending, it's complete.",
|
||||
o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{
|
||||
AnnotationKeyExternalCreateFailed: evenEarlier,
|
||||
AnnotationKeyExternalCreatePending: earlier,
|
||||
AnnotationKeyExternalCreateSucceeded: now,
|
||||
}}},
|
||||
want: false,
|
||||
},
|
||||
"CreateFailed": {
|
||||
reason: "If Create failed since it was pending, it's complete.",
|
||||
o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{
|
||||
AnnotationKeyExternalCreateSucceeded: evenEarlier,
|
||||
AnnotationKeyExternalCreatePending: earlier,
|
||||
AnnotationKeyExternalCreateFailed: now,
|
||||
}}},
|
||||
want: false,
|
||||
},
|
||||
"CreateNeverCompleted": {
|
||||
reason: "If Create was pending but never succeeded or failed, it's incomplete.",
|
||||
o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{
|
||||
AnnotationKeyExternalCreatePending: earlier,
|
||||
}}},
|
||||
want: true,
|
||||
},
|
||||
"RecreateNeverCompleted": {
|
||||
reason: "If Create is pending and there's an older success we're probably trying to recreate a deleted external resource, and it's incomplete.",
|
||||
o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{
|
||||
AnnotationKeyExternalCreateSucceeded: earlier,
|
||||
AnnotationKeyExternalCreatePending: now,
|
||||
}}},
|
||||
want: true,
|
||||
},
|
||||
"RetryNeverCompleted": {
|
||||
reason: "If Create is pending and there's an older failure we're probably trying to recreate a deleted external resource, and it's incomplete.",
|
||||
o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{
|
||||
AnnotationKeyExternalCreateFailed: earlier,
|
||||
AnnotationKeyExternalCreatePending: now,
|
||||
}}},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got := ExternalCreateIncomplete(tc.o)
|
||||
if diff := cmp.Diff(tc.want, got); diff != "" {
|
||||
t.Errorf("ExternalCreateIncomplete(...): -want, +got:\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllowPropagation(t *testing.T) {
|
||||
fromns := "from-namespace"
|
||||
from := "from-name"
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ import (
|
|||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/pkg/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/util/retry"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
|
||||
|
|
@ -31,10 +33,11 @@ import (
|
|||
|
||||
// Error strings.
|
||||
const (
|
||||
errCreateOrUpdateSecret = "cannot create or update connection secret"
|
||||
errUpdateManaged = "cannot update managed resource"
|
||||
errUpdateManagedStatus = "cannot update managed resource status"
|
||||
errResolveReferences = "cannot resolve references"
|
||||
errCreateOrUpdateSecret = "cannot create or update connection secret"
|
||||
errUpdateManaged = "cannot update managed resource"
|
||||
errUpdateManagedStatus = "cannot update managed resource status"
|
||||
errResolveReferences = "cannot resolve references"
|
||||
errUpdateCriticalAnnotations = "cannot update critical annotations"
|
||||
)
|
||||
|
||||
// NameAsExternalName writes the name of the managed resource to
|
||||
|
|
@ -152,3 +155,33 @@ func (a *APISimpleReferenceResolver) ResolveReferences(ctx context.Context, mg r
|
|||
|
||||
return errors.Wrap(a.client.Update(ctx, mg), errUpdateManaged)
|
||||
}
|
||||
|
||||
// A RetryingCriticalAnnotationUpdater is a CriticalAnnotationUpdater that
|
||||
// retries annotation updates in the face of API server errors.
|
||||
type RetryingCriticalAnnotationUpdater struct {
|
||||
client client.Client
|
||||
}
|
||||
|
||||
// NewRetryingCriticalAnnotationUpdater returns a CriticalAnnotationUpdater that
|
||||
// retries annotation updates in the face of API server errors.
|
||||
func NewRetryingCriticalAnnotationUpdater(c client.Client) *RetryingCriticalAnnotationUpdater {
|
||||
return &RetryingCriticalAnnotationUpdater{client: c}
|
||||
}
|
||||
|
||||
// UpdateCriticalAnnotations updates (i.e. persists) the annotations of the
|
||||
// supplied Object. It retries in the face of any API server error several times
|
||||
// in order to ensure annotations that contain critical state are persisted. Any
|
||||
// pending changes to the supplied Object's spec, status, or other metadata are
|
||||
// reset to their current state according to the API server.
|
||||
func (u *RetryingCriticalAnnotationUpdater) UpdateCriticalAnnotations(ctx context.Context, o client.Object) error {
|
||||
a := o.GetAnnotations()
|
||||
err := retry.OnError(retry.DefaultRetry, resource.IsAPIError, func() error {
|
||||
nn := types.NamespacedName{Name: o.GetName()}
|
||||
if err := u.client.Get(ctx, nn, o); err != nil {
|
||||
return err
|
||||
}
|
||||
meta.AddAnnotations(o, a)
|
||||
return u.client.Update(ctx, o)
|
||||
})
|
||||
return errors.Wrap(err, errUpdateCriticalAnnotations)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -377,7 +377,67 @@ func TestResolveReferences(t *testing.T) {
|
|||
r := NewAPISimpleReferenceResolver(tc.c)
|
||||
got := r.ResolveReferences(tc.args.ctx, tc.args.mg)
|
||||
if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" {
|
||||
t.Errorf("\nReason: %s\r.ResolveReferences(...): -want, +got:\n%s", tc.reason, diff)
|
||||
t.Errorf("\n%s\nr.ResolveReferences(...): -want, +got:\n%s", tc.reason, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetryingCriticalAnnotationUpdater(t *testing.T) {
|
||||
|
||||
errBoom := errors.New("boom")
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
o client.Object
|
||||
}
|
||||
|
||||
cases := map[string]struct {
|
||||
reason string
|
||||
c client.Client
|
||||
args args
|
||||
want error
|
||||
}{
|
||||
"GetError": {
|
||||
reason: "We should return any error we encounter getting the supplied object",
|
||||
c: &test.MockClient{
|
||||
MockGet: test.NewMockGetFn(errBoom),
|
||||
},
|
||||
args: args{
|
||||
o: &fake.Managed{},
|
||||
},
|
||||
want: errors.Wrap(errBoom, errUpdateCriticalAnnotations),
|
||||
},
|
||||
"UpdateError": {
|
||||
reason: "We should return any error we encounter updating the supplied object",
|
||||
c: &test.MockClient{
|
||||
MockGet: test.NewMockGetFn(nil),
|
||||
MockUpdate: test.NewMockUpdateFn(errBoom),
|
||||
},
|
||||
args: args{
|
||||
o: &fake.Managed{},
|
||||
},
|
||||
want: errors.Wrap(errBoom, errUpdateCriticalAnnotations),
|
||||
},
|
||||
"Success": {
|
||||
reason: "We should return without error if we successfully update our annotations",
|
||||
c: &test.MockClient{
|
||||
MockGet: test.NewMockGetFn(nil),
|
||||
MockUpdate: test.NewMockUpdateFn(errBoom),
|
||||
},
|
||||
args: args{
|
||||
o: &fake.Managed{},
|
||||
},
|
||||
want: errors.Wrap(errBoom, errUpdateCriticalAnnotations),
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
u := NewRetryingCriticalAnnotationUpdater(tc.c)
|
||||
got := u.UpdateCriticalAnnotations(tc.args.ctx, tc.args.o)
|
||||
if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" {
|
||||
t.Errorf("\n%s\nu.UpdateCriticalAnnotations(...): -want, +got:\n%s", tc.reason, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,9 +21,6 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/util/retry"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
|
@ -43,12 +40,14 @@ const (
|
|||
reconcileTimeout = 1 * time.Minute
|
||||
|
||||
defaultpollInterval = 1 * time.Minute
|
||||
defaultGracePeriod = 30 * time.Second
|
||||
)
|
||||
|
||||
// Error strings.
|
||||
const (
|
||||
errGetManaged = "cannot get managed resource"
|
||||
errUpdateManagedAfterCreate = "cannot update managed resource. this may have resulted in a leaked external resource"
|
||||
errUpdateManagedAnnotations = "cannot update managed resource annotations"
|
||||
errCreateIncomplete = "cannot determine creation result - remove the " + meta.AnnotationKeyExternalCreatePending + " annotation if it is safe to proceed"
|
||||
errReconcileConnect = "connect failed"
|
||||
errReconcileObserve = "observe failed"
|
||||
errReconcileCreate = "create failed"
|
||||
|
|
@ -72,6 +71,7 @@ const (
|
|||
reasonDeleted event.Reason = "DeletedExternalResource"
|
||||
reasonCreated event.Reason = "CreatedExternalResource"
|
||||
reasonUpdated event.Reason = "UpdatedExternalResource"
|
||||
reasonPending event.Reason = "PendingExternalResource"
|
||||
)
|
||||
|
||||
// ControllerName returns the recommended name for controllers that use this
|
||||
|
|
@ -80,6 +80,21 @@ func ControllerName(kind string) string {
|
|||
return "managed/" + strings.ToLower(kind)
|
||||
}
|
||||
|
||||
// A CriticalAnnotationUpdater is used when it is critical that annotations must
|
||||
// be updated before returning from the Reconcile loop.
|
||||
type CriticalAnnotationUpdater interface {
|
||||
UpdateCriticalAnnotations(ctx context.Context, o client.Object) error
|
||||
}
|
||||
|
||||
// A CriticalAnnotationUpdateFn may be used when it is critical that annotations
|
||||
// must be updated before returning from the Reconcile loop.
|
||||
type CriticalAnnotationUpdateFn func(ctx context.Context, o client.Object) error
|
||||
|
||||
// UpdateCriticalAnnotations of the supplied object.
|
||||
func (fn CriticalAnnotationUpdateFn) UpdateCriticalAnnotations(ctx context.Context, o client.Object) error {
|
||||
return fn(ctx, o)
|
||||
}
|
||||
|
||||
// ConnectionDetails created or updated during an operation on an external
|
||||
// resource, for example usernames, passwords, endpoints, ports, etc.
|
||||
type ConnectionDetails map[string][]byte
|
||||
|
|
@ -185,15 +200,19 @@ func (ec ExternalConnectorFn) Connect(ctx context.Context, mg resource.Managed)
|
|||
// if it's called again with the same parameters or Delete call should not
|
||||
// return error if there is an ongoing deletion or resource does not exist.
|
||||
type ExternalClient interface {
|
||||
// Observe the external resource the supplied Managed resource represents,
|
||||
// if any. Observe implementations must not modify the external resource,
|
||||
// but may update the supplied Managed resource to reflect the state of the
|
||||
// external resource.
|
||||
// Observe the external resource the supplied Managed resource
|
||||
// represents, if any. Observe implementations must not modify the
|
||||
// external resource, but may update the supplied Managed resource to
|
||||
// reflect the state of the external resource. Status modifications are
|
||||
// automatically persisted unless ResourceLateInitialized is true - see
|
||||
// ResourceLateInitialized for more detail.
|
||||
Observe(ctx context.Context, mg resource.Managed) (ExternalObservation, error)
|
||||
|
||||
// Create an external resource per the specifications of the supplied
|
||||
// Managed resource. Called when Observe reports that the associated
|
||||
// external resource does not exist.
|
||||
// external resource does not exist. Create implementations may update
|
||||
// managed resource annotations, and those updates will be persisted.
|
||||
// All other updates will be discarded.
|
||||
Create(ctx context.Context, mg resource.Managed) (ExternalCreation, error)
|
||||
|
||||
// Update the external resource represented by the supplied Managed
|
||||
|
|
@ -316,10 +335,14 @@ type ExternalObservation struct {
|
|||
|
||||
// An ExternalCreation is the result of the creation of an external resource.
|
||||
type ExternalCreation struct {
|
||||
// ExternalNameAssigned is true if the Create operation resulted in a change
|
||||
// in the external name annotation. If that's the case, we need to issue a
|
||||
// spec update and make sure it goes through so that we don't lose the identifier
|
||||
// of the resource we just created.
|
||||
// ExternalNameAssigned should be true if the Create operation resulted
|
||||
// in a change in the resource's external name. This is typically only
|
||||
// needed for external resource's with unpredictable external names that
|
||||
// are returned from the API at create time.
|
||||
//
|
||||
// Deprecated: The managed.Reconciler no longer needs to be informed
|
||||
// when an external name is assigned by the Create operation. It will
|
||||
// automatically detect and handle external name assignment.
|
||||
ExternalNameAssigned bool
|
||||
|
||||
// ConnectionDetails required to connect to this resource. These details
|
||||
|
|
@ -348,8 +371,9 @@ type Reconciler struct {
|
|||
client client.Client
|
||||
newManaged func() resource.Managed
|
||||
|
||||
pollInterval time.Duration
|
||||
timeout time.Duration
|
||||
pollInterval time.Duration
|
||||
timeout time.Duration
|
||||
creationGracePeriod time.Duration
|
||||
|
||||
// The below structs embed the set of interfaces used to implement the
|
||||
// managed resource reconciler. We do this primarily for readability, so
|
||||
|
|
@ -363,6 +387,7 @@ type Reconciler struct {
|
|||
}
|
||||
|
||||
type mrManaged struct {
|
||||
CriticalAnnotationUpdater
|
||||
ConnectionPublisher
|
||||
resource.Finalizer
|
||||
Initializer
|
||||
|
|
@ -371,10 +396,11 @@ type mrManaged struct {
|
|||
|
||||
func defaultMRManaged(m manager.Manager) mrManaged {
|
||||
return mrManaged{
|
||||
ConnectionPublisher: NewAPISecretPublisher(m.GetClient(), m.GetScheme()),
|
||||
Finalizer: resource.NewAPIFinalizer(m.GetClient(), managedFinalizerName),
|
||||
Initializer: NewNameAsExternalName(m.GetClient()),
|
||||
ReferenceResolver: NewAPISimpleReferenceResolver(m.GetClient()),
|
||||
CriticalAnnotationUpdater: NewRetryingCriticalAnnotationUpdater(m.GetClient()),
|
||||
ConnectionPublisher: NewAPISecretPublisher(m.GetClient(), m.GetScheme()),
|
||||
Finalizer: resource.NewAPIFinalizer(m.GetClient(), managedFinalizerName),
|
||||
Initializer: NewNameAsExternalName(m.GetClient()),
|
||||
ReferenceResolver: NewAPISimpleReferenceResolver(m.GetClient()),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -412,6 +438,17 @@ func WithPollInterval(after time.Duration) ReconcilerOption {
|
|||
}
|
||||
}
|
||||
|
||||
// WithCreationGracePeriod configures an optional period during which we will
|
||||
// wait for the external API to report that a newly created external resource
|
||||
// exists. This allows us to tolerate eventually consistent APIs that do not
|
||||
// immediately report that newly created resources exist when queried. All
|
||||
// resources have a 30 second grace period by default.
|
||||
func WithCreationGracePeriod(d time.Duration) ReconcilerOption {
|
||||
return func(r *Reconciler) {
|
||||
r.creationGracePeriod = d
|
||||
}
|
||||
}
|
||||
|
||||
// WithExternalConnecter specifies how the Reconciler should connect to the API
|
||||
// used to sync and delete external resources.
|
||||
func WithExternalConnecter(c ExternalConnecter) ReconcilerOption {
|
||||
|
|
@ -420,6 +457,16 @@ func WithExternalConnecter(c ExternalConnecter) ReconcilerOption {
|
|||
}
|
||||
}
|
||||
|
||||
// WithCriticalAnnotationUpdater specifies how the Reconciler should update a
|
||||
// managed resource's critical annotations. Implementations typically contain
|
||||
// some kind of retry logic to increase the likelihood that critical annotations
|
||||
// (like non-deterministic external names) will be persisted.
|
||||
func WithCriticalAnnotationUpdater(u CriticalAnnotationUpdater) ReconcilerOption {
|
||||
return func(r *Reconciler) {
|
||||
r.managed.CriticalAnnotationUpdater = u
|
||||
}
|
||||
}
|
||||
|
||||
// WithConnectionPublishers specifies how the Reconciler should publish
|
||||
// its connection details such as credentials and endpoints.
|
||||
func WithConnectionPublishers(p ...ConnectionPublisher) ReconcilerOption {
|
||||
|
|
@ -483,14 +530,15 @@ func NewReconciler(m manager.Manager, of resource.ManagedKind, o ...ReconcilerOp
|
|||
_ = nm()
|
||||
|
||||
r := &Reconciler{
|
||||
client: m.GetClient(),
|
||||
newManaged: nm,
|
||||
pollInterval: defaultpollInterval,
|
||||
timeout: reconcileTimeout,
|
||||
managed: defaultMRManaged(m),
|
||||
external: defaultMRExternal(),
|
||||
log: logging.NewNopLogger(),
|
||||
record: event.NewNopRecorder(),
|
||||
client: m.GetClient(),
|
||||
newManaged: nm,
|
||||
pollInterval: defaultpollInterval,
|
||||
creationGracePeriod: defaultGracePeriod,
|
||||
timeout: reconcileTimeout,
|
||||
managed: defaultMRManaged(m),
|
||||
external: defaultMRExternal(),
|
||||
log: logging.NewNopLogger(),
|
||||
record: event.NewNopRecorder(),
|
||||
}
|
||||
|
||||
for _, ro := range o {
|
||||
|
|
@ -581,6 +629,17 @@ func (r *Reconciler) Reconcile(_ context.Context, req reconcile.Request) (reconc
|
|||
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
|
||||
}
|
||||
|
||||
// If we started but never completed creation of an external resource we
|
||||
// may have lost critical information. For example if we didn't persist
|
||||
// an updated external name we've leaked a resource. The safest thing to
|
||||
// do is to refuse to proceed.
|
||||
if meta.ExternalCreateIncomplete(managed) {
|
||||
log.Debug(errCreateIncomplete)
|
||||
record.Event(managed, event.Warning(reasonCannotInitialize, errors.New(errCreateIncomplete)))
|
||||
managed.SetConditions(xpv1.ReconcileError(errors.New(errCreateIncomplete)))
|
||||
return reconcile.Result{Requeue: false}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
|
||||
}
|
||||
|
||||
// We resolve any references before observing our external resource because
|
||||
// in some rare examples we need a spec field to make the observe call, and
|
||||
// that spec field could be set by a reference.
|
||||
|
|
@ -631,6 +690,17 @@ func (r *Reconciler) Reconcile(_ context.Context, req reconcile.Request) (reconc
|
|||
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
|
||||
}
|
||||
|
||||
// If this resource has a non-zero creation grace period we want to wait
|
||||
// for that period to expire before we trust that the resource really
|
||||
// doesn't exist. This is because some external APIs are eventually
|
||||
// consistent and may report that a recently created resource does not
|
||||
// exist.
|
||||
if !observation.ResourceExists && meta.ExternalCreateSucceededDuring(managed, r.creationGracePeriod) {
|
||||
log.Debug("Waiting for external resource existence to be confirmed")
|
||||
record.Event(managed, event.Normal(reasonPending, "Waiting for external resource existence to be confirmed"))
|
||||
return reconcile.Result{Requeue: true}, nil
|
||||
}
|
||||
|
||||
if meta.WasDeleted(managed) {
|
||||
log = log.WithValues("deletion-timestamp", managed.GetDeletionTimestamp())
|
||||
managed.SetConditions(xpv1.Deleting())
|
||||
|
|
@ -711,6 +781,21 @@ func (r *Reconciler) Reconcile(_ context.Context, req reconcile.Request) (reconc
|
|||
}
|
||||
|
||||
if !observation.ResourceExists {
|
||||
// We write this annotation for two reasons. Firstly, it helps
|
||||
// us to detect the case in which we fail to persist critical
|
||||
// information (like the external name) that may be set by the
|
||||
// subsequent external.Create call. Secondly, it guarantees that
|
||||
// we're operating on the latest version of our resource. We
|
||||
// don't use the CriticalAnnotationUpdater because we _want_ the
|
||||
// update to fail if we get a 409 due to a stale version.
|
||||
meta.SetExternalCreatePending(managed, time.Now())
|
||||
if err := r.client.Update(ctx, managed); err != nil {
|
||||
log.Debug(errUpdateManaged, "error", err)
|
||||
record.Event(managed, event.Warning(reasonCannotUpdateManaged, errors.Wrap(err, errUpdateManaged)))
|
||||
managed.SetConditions(xpv1.ReconcileError(errors.Wrap(err, errUpdateManaged)))
|
||||
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
|
||||
}
|
||||
|
||||
managed.SetConditions(xpv1.Creating())
|
||||
creation, err := external.Create(externalCtx, managed)
|
||||
if err != nil {
|
||||
|
|
@ -721,30 +806,49 @@ func (r *Reconciler) Reconcile(_ context.Context, req reconcile.Request) (reconc
|
|||
// the new error condition. If not, we requeue explicitly, which will trigger backoff.
|
||||
log.Debug("Cannot create external resource", "error", err)
|
||||
record.Event(managed, event.Warning(reasonCannotCreate, err))
|
||||
|
||||
// We handle annotations specially here because it's
|
||||
// critical that they are persisted to the API server.
|
||||
// If we don't add the external-create-failed annotation
|
||||
// the reconciler will refuse to proceed, because it
|
||||
// won't know whether or not it created an external
|
||||
// resource.
|
||||
meta.SetExternalCreateFailed(managed, time.Now())
|
||||
if err := r.managed.UpdateCriticalAnnotations(ctx, managed); err != nil {
|
||||
log.Debug(errUpdateManagedAnnotations, "error", err)
|
||||
record.Event(managed, event.Warning(reasonCannotUpdateManaged, errors.Wrap(err, errUpdateManagedAnnotations)))
|
||||
|
||||
// We only log and emit an event here rather
|
||||
// than setting a status condition and returning
|
||||
// early because presumably it's more useful to
|
||||
// set our status condition to the reason the
|
||||
// create failed.
|
||||
}
|
||||
|
||||
managed.SetConditions(xpv1.ReconcileError(errors.Wrap(err, errReconcileCreate)))
|
||||
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
|
||||
}
|
||||
|
||||
if creation.ExternalNameAssigned {
|
||||
en := meta.GetExternalName(managed)
|
||||
// We will retry in all cases where the error comes from the api-server.
|
||||
// At one point, context deadline will be exceeded and we'll get out
|
||||
// of the loop. In that case, we warn the user that the external resource
|
||||
// might be leaked.
|
||||
err := retry.OnError(retry.DefaultRetry, resource.IsAPIError, func() error {
|
||||
nn := types.NamespacedName{Name: managed.GetName()}
|
||||
if err := r.client.Get(ctx, nn, managed); err != nil {
|
||||
return err
|
||||
}
|
||||
meta.SetExternalName(managed, en)
|
||||
return r.client.Update(ctx, managed)
|
||||
})
|
||||
if err != nil {
|
||||
log.Debug("Cannot update managed resource", "error", err)
|
||||
record.Event(managed, event.Warning(reasonCannotUpdateManaged, errors.Wrap(err, errUpdateManagedAfterCreate)))
|
||||
managed.SetConditions(xpv1.ReconcileError(errors.Wrap(err, errUpdateManagedAfterCreate)))
|
||||
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
|
||||
}
|
||||
// In some cases our external-name may be set by Create above.
|
||||
log = log.WithValues("external-name", meta.GetExternalName(managed))
|
||||
record = r.record.WithAnnotations("external-name", meta.GetExternalName(managed))
|
||||
|
||||
// We handle annotations specially here because it's critical
|
||||
// that they are persisted to the API server. If we don't remove
|
||||
// add the external-create-succeeded annotation the reconciler
|
||||
// will refuse to proceed, because it won't know whether or not
|
||||
// it created an external resource. This is also important in
|
||||
// cases where we must record an external-name annotation set by
|
||||
// the Create call. Any other changes made during Create will be
|
||||
// reverted when annotations are updated; at the time of writing
|
||||
// Create implementations are advised not to alter status, but
|
||||
// we may revisit this in future.
|
||||
meta.SetExternalCreateSucceeded(managed, time.Now())
|
||||
if err := r.managed.UpdateCriticalAnnotations(ctx, managed); err != nil {
|
||||
log.Debug(errUpdateManagedAnnotations, "error", err)
|
||||
record.Event(managed, event.Warning(reasonCannotUpdateManaged, errors.Wrap(err, errUpdateManagedAnnotations)))
|
||||
managed.SetConditions(xpv1.ReconcileError(errors.Wrap(err, errUpdateManagedAnnotations)))
|
||||
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
|
||||
}
|
||||
|
||||
if err := r.managed.PublishConnection(ctx, managed, creation.ConnectionDetails); err != nil {
|
||||
|
|
|
|||
|
|
@ -19,12 +19,14 @@ package managed
|
|||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/pkg/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/manager"
|
||||
|
|
@ -199,6 +201,35 @@ func TestReconciler(t *testing.T) {
|
|||
},
|
||||
want: want{result: reconcile.Result{Requeue: true}},
|
||||
},
|
||||
"ExternalCreatePending": {
|
||||
reason: "We should return early if the managed resource appears to be pending creation. We might have leaked a resource and don't want to create another.",
|
||||
args: args{
|
||||
m: &fake.Manager{
|
||||
Client: &test.MockClient{
|
||||
MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
|
||||
meta.SetExternalCreatePending(obj, now.Time)
|
||||
return nil
|
||||
}),
|
||||
MockStatusUpdate: test.MockStatusUpdateFn(func(_ context.Context, obj client.Object, _ ...client.UpdateOption) error {
|
||||
want := &fake.Managed{}
|
||||
meta.SetExternalCreatePending(want, now.Time)
|
||||
want.SetConditions(xpv1.ReconcileError(errors.New(errCreateIncomplete)))
|
||||
if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" {
|
||||
reason := "We should update our status when we're asked to reconcile a managed resource that is pending creation."
|
||||
t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff)
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
},
|
||||
Scheme: fake.SchemeWith(&fake.Managed{}),
|
||||
},
|
||||
mg: resource.ManagedKind(fake.GVK(&fake.Managed{})),
|
||||
o: []ReconcilerOption{
|
||||
WithInitializers(InitializerFn(func(_ context.Context, mg resource.Managed) error { return nil })),
|
||||
},
|
||||
},
|
||||
want: want{result: reconcile.Result{Requeue: false}},
|
||||
},
|
||||
"ResolveReferencesError": {
|
||||
reason: "Errors during reference resolution references should trigger a requeue after a short wait.",
|
||||
args: args{
|
||||
|
|
@ -288,6 +319,34 @@ func TestReconciler(t *testing.T) {
|
|||
},
|
||||
want: want{result: reconcile.Result{Requeue: true}},
|
||||
},
|
||||
"CreationGracePeriod": {
|
||||
reason: "If our resource appears not to exist during the creation grace period we should return early.",
|
||||
args: args{
|
||||
m: &fake.Manager{
|
||||
Client: &test.MockClient{
|
||||
MockGet: test.NewMockGetFn(nil, func(obj client.Object) error {
|
||||
meta.SetExternalCreateSucceeded(obj, time.Now())
|
||||
return nil
|
||||
}),
|
||||
},
|
||||
Scheme: fake.SchemeWith(&fake.Managed{}),
|
||||
},
|
||||
mg: resource.ManagedKind(fake.GVK(&fake.Managed{})),
|
||||
o: []ReconcilerOption{
|
||||
WithInitializers(),
|
||||
WithCreationGracePeriod(1 * time.Minute),
|
||||
WithExternalConnecter(ExternalConnectorFn(func(_ context.Context, mg resource.Managed) (ExternalClient, error) {
|
||||
c := &ExternalClientFns{
|
||||
ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) {
|
||||
return ExternalObservation{ResourceExists: false}, nil
|
||||
},
|
||||
}
|
||||
return c, nil
|
||||
})),
|
||||
},
|
||||
},
|
||||
want: want{result: reconcile.Result{Requeue: true}},
|
||||
},
|
||||
"ExternalDeleteError": {
|
||||
reason: "Errors deleting the external resource should trigger a requeue after a short wait.",
|
||||
args: args{
|
||||
|
|
@ -558,17 +617,18 @@ func TestReconciler(t *testing.T) {
|
|||
},
|
||||
want: want{result: reconcile.Result{Requeue: true}},
|
||||
},
|
||||
"CreateExternalError": {
|
||||
reason: "Errors while creating an external resource should trigger a requeue after a short wait.",
|
||||
"UpdateCreatePendingError": {
|
||||
reason: "Errors while updating our external-create-pending annotation should trigger a requeue after a short wait.",
|
||||
args: args{
|
||||
m: &fake.Manager{
|
||||
Client: &test.MockClient{
|
||||
MockGet: test.NewMockGetFn(nil),
|
||||
MockGet: test.NewMockGetFn(nil),
|
||||
MockUpdate: test.NewMockUpdateFn(errBoom),
|
||||
MockStatusUpdate: test.MockStatusUpdateFn(func(_ context.Context, obj client.Object, _ ...client.UpdateOption) error {
|
||||
want := &fake.Managed{}
|
||||
want.SetConditions(xpv1.ReconcileError(errors.Wrap(errBoom, errReconcileCreate)))
|
||||
want.SetConditions(xpv1.Creating())
|
||||
if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" {
|
||||
meta.SetExternalCreatePending(want, time.Now())
|
||||
want.SetConditions(xpv1.ReconcileError(errors.Wrap(errBoom, errUpdateManaged)))
|
||||
if diff := cmp.Diff(want, obj, test.EquateConditions(), cmpopts.EquateApproxTime(1*time.Second)); diff != "" {
|
||||
reason := "Errors while creating an external resource should be reported as a conditioned status."
|
||||
t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff)
|
||||
}
|
||||
|
|
@ -598,17 +658,109 @@ func TestReconciler(t *testing.T) {
|
|||
},
|
||||
want: want{result: reconcile.Result{Requeue: true}},
|
||||
},
|
||||
"CreateExternalError": {
|
||||
reason: "Errors while creating an external resource should trigger a requeue after a short wait.",
|
||||
args: args{
|
||||
m: &fake.Manager{
|
||||
Client: &test.MockClient{
|
||||
MockGet: test.NewMockGetFn(nil),
|
||||
MockUpdate: test.NewMockUpdateFn(nil),
|
||||
MockStatusUpdate: test.MockStatusUpdateFn(func(_ context.Context, obj client.Object, _ ...client.UpdateOption) error {
|
||||
want := &fake.Managed{}
|
||||
meta.SetExternalCreatePending(want, time.Now())
|
||||
meta.SetExternalCreateFailed(want, time.Now())
|
||||
want.SetConditions(xpv1.ReconcileError(errors.Wrap(errBoom, errReconcileCreate)))
|
||||
want.SetConditions(xpv1.Creating())
|
||||
if diff := cmp.Diff(want, obj, test.EquateConditions(), cmpopts.EquateApproxTime(1*time.Second)); diff != "" {
|
||||
reason := "Errors while creating an external resource should be reported as a conditioned status."
|
||||
t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff)
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
},
|
||||
Scheme: fake.SchemeWith(&fake.Managed{}),
|
||||
},
|
||||
mg: resource.ManagedKind(fake.GVK(&fake.Managed{})),
|
||||
o: []ReconcilerOption{
|
||||
WithInitializers(),
|
||||
WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })),
|
||||
WithExternalConnecter(ExternalConnectorFn(func(_ context.Context, mg resource.Managed) (ExternalClient, error) {
|
||||
c := &ExternalClientFns{
|
||||
ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) {
|
||||
return ExternalObservation{ResourceExists: false}, nil
|
||||
},
|
||||
CreateFn: func(_ context.Context, _ resource.Managed) (ExternalCreation, error) {
|
||||
return ExternalCreation{}, errBoom
|
||||
},
|
||||
}
|
||||
return c, nil
|
||||
})),
|
||||
// We simulate our critical annotation update failing too here.
|
||||
// This is mostly just to exercise the code, which just creates a log and an event.
|
||||
WithCriticalAnnotationUpdater(CriticalAnnotationUpdateFn(func(ctx context.Context, o client.Object) error { return errBoom })),
|
||||
WithConnectionPublishers(),
|
||||
WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}),
|
||||
},
|
||||
},
|
||||
want: want{result: reconcile.Result{Requeue: true}},
|
||||
},
|
||||
"UpdateCriticalAnnotationsError": {
|
||||
reason: "Errors updating critical annotations after creation should trigger a requeue after a short wait.",
|
||||
args: args{
|
||||
m: &fake.Manager{
|
||||
Client: &test.MockClient{
|
||||
MockGet: test.NewMockGetFn(nil),
|
||||
MockUpdate: test.NewMockUpdateFn(nil),
|
||||
MockStatusUpdate: test.MockStatusUpdateFn(func(_ context.Context, obj client.Object, _ ...client.UpdateOption) error {
|
||||
want := &fake.Managed{}
|
||||
meta.SetExternalCreatePending(want, time.Now())
|
||||
meta.SetExternalCreateSucceeded(want, time.Now())
|
||||
want.SetConditions(xpv1.ReconcileError(errors.Wrap(errBoom, errUpdateManagedAnnotations)))
|
||||
want.SetConditions(xpv1.Creating())
|
||||
if diff := cmp.Diff(want, obj, test.EquateConditions(), cmpopts.EquateApproxTime(1*time.Second)); diff != "" {
|
||||
reason := "Errors updating critical annotations after creation should be reported as a conditioned status."
|
||||
t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff)
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
},
|
||||
Scheme: fake.SchemeWith(&fake.Managed{}),
|
||||
},
|
||||
mg: resource.ManagedKind(fake.GVK(&fake.Managed{})),
|
||||
o: []ReconcilerOption{
|
||||
WithInitializers(),
|
||||
WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })),
|
||||
WithExternalConnecter(ExternalConnectorFn(func(_ context.Context, mg resource.Managed) (ExternalClient, error) {
|
||||
c := &ExternalClientFns{
|
||||
ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) {
|
||||
return ExternalObservation{ResourceExists: false}, nil
|
||||
},
|
||||
CreateFn: func(_ context.Context, _ resource.Managed) (ExternalCreation, error) {
|
||||
return ExternalCreation{}, nil
|
||||
},
|
||||
}
|
||||
return c, nil
|
||||
})),
|
||||
WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}),
|
||||
WithCriticalAnnotationUpdater(CriticalAnnotationUpdateFn(func(ctx context.Context, o client.Object) error { return errBoom })),
|
||||
},
|
||||
},
|
||||
want: want{result: reconcile.Result{Requeue: true}},
|
||||
},
|
||||
"PublishCreationConnectionDetailsError": {
|
||||
reason: "Errors publishing connection details after creation should trigger a requeue after a short wait.",
|
||||
args: args{
|
||||
m: &fake.Manager{
|
||||
Client: &test.MockClient{
|
||||
MockGet: test.NewMockGetFn(nil),
|
||||
MockGet: test.NewMockGetFn(nil),
|
||||
MockUpdate: test.NewMockUpdateFn(nil),
|
||||
MockStatusUpdate: test.MockStatusUpdateFn(func(_ context.Context, obj client.Object, _ ...client.UpdateOption) error {
|
||||
want := &fake.Managed{}
|
||||
meta.SetExternalCreatePending(want, time.Now())
|
||||
meta.SetExternalCreateSucceeded(want, time.Now())
|
||||
want.SetConditions(xpv1.ReconcileError(errBoom))
|
||||
want.SetConditions(xpv1.Creating())
|
||||
if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" {
|
||||
if diff := cmp.Diff(want, obj, test.EquateConditions(), cmpopts.EquateApproxTime(1*time.Second)); diff != "" {
|
||||
reason := "Errors publishing connection details after creation should be reported as a conditioned status."
|
||||
t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff)
|
||||
}
|
||||
|
|
@ -633,6 +785,7 @@ func TestReconciler(t *testing.T) {
|
|||
}
|
||||
return c, nil
|
||||
})),
|
||||
WithCriticalAnnotationUpdater(CriticalAnnotationUpdateFn(func(ctx context.Context, o client.Object) error { return nil })),
|
||||
WithConnectionPublishers(ConnectionPublisherFns{
|
||||
PublishConnectionFn: func(_ context.Context, _ resource.Managed, cd ConnectionDetails) error {
|
||||
// We're called after observe, create, and update
|
||||
|
|
@ -654,12 +807,15 @@ func TestReconciler(t *testing.T) {
|
|||
args: args{
|
||||
m: &fake.Manager{
|
||||
Client: &test.MockClient{
|
||||
MockGet: test.NewMockGetFn(nil),
|
||||
MockGet: test.NewMockGetFn(nil),
|
||||
MockUpdate: test.NewMockUpdateFn(nil),
|
||||
MockStatusUpdate: test.MockStatusUpdateFn(func(_ context.Context, obj client.Object, _ ...client.UpdateOption) error {
|
||||
want := &fake.Managed{}
|
||||
meta.SetExternalCreatePending(want, time.Now())
|
||||
meta.SetExternalCreateSucceeded(want, time.Now())
|
||||
want.SetConditions(xpv1.ReconcileSuccess())
|
||||
want.SetConditions(xpv1.Creating())
|
||||
if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" {
|
||||
if diff := cmp.Diff(want, obj, test.EquateConditions(), cmpopts.EquateApproxTime(1*time.Second)); diff != "" {
|
||||
reason := "Successful managed resource creation should be reported as a conditioned status."
|
||||
t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff)
|
||||
}
|
||||
|
|
@ -673,139 +829,7 @@ func TestReconciler(t *testing.T) {
|
|||
WithInitializers(),
|
||||
WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })),
|
||||
WithExternalConnecter(&NopConnecter{}),
|
||||
WithConnectionPublishers(),
|
||||
WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}),
|
||||
},
|
||||
},
|
||||
want: want{result: reconcile.Result{Requeue: true}},
|
||||
},
|
||||
"CreateWithExternalNameAssignmentSuccessful": {
|
||||
reason: "Successful managed resource creation with external name assignment should trigger an update.",
|
||||
args: args{
|
||||
m: &fake.Manager{
|
||||
Client: &test.MockClient{
|
||||
MockGet: test.NewMockGetFn(nil),
|
||||
MockUpdate: test.NewMockUpdateFn(nil),
|
||||
MockStatusUpdate: test.MockStatusUpdateFn(func(_ context.Context, obj client.Object, _ ...client.UpdateOption) error {
|
||||
want := &fake.Managed{}
|
||||
meta.SetExternalName(want, "test")
|
||||
want.SetConditions(xpv1.ReconcileSuccess())
|
||||
want.SetConditions(xpv1.Creating())
|
||||
if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" {
|
||||
reason := "Successful managed resource creation should be reported as a conditioned status."
|
||||
t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff)
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
},
|
||||
Scheme: fake.SchemeWith(&fake.Managed{}),
|
||||
},
|
||||
mg: resource.ManagedKind(fake.GVK(&fake.Managed{})),
|
||||
o: []ReconcilerOption{
|
||||
WithInitializers(),
|
||||
WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })),
|
||||
WithExternalConnecter(ExternalConnectorFn(func(_ context.Context, mg resource.Managed) (ExternalClient, error) {
|
||||
c := &ExternalClientFns{
|
||||
CreateFn: func(_ context.Context, mg resource.Managed) (ExternalCreation, error) {
|
||||
meta.SetExternalName(mg, "test")
|
||||
return ExternalCreation{ExternalNameAssigned: true}, nil
|
||||
},
|
||||
ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) {
|
||||
return ExternalObservation{}, nil
|
||||
},
|
||||
}
|
||||
return c, nil
|
||||
})),
|
||||
WithConnectionPublishers(),
|
||||
WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}),
|
||||
},
|
||||
},
|
||||
want: want{result: reconcile.Result{Requeue: true}},
|
||||
},
|
||||
"CreateWithExternalNameAssignmentGetError": {
|
||||
reason: "If the Get call during the update after Create does not go through, we need to inform the user and requeue shortly.",
|
||||
args: args{
|
||||
m: &fake.Manager{
|
||||
Client: &test.MockClient{
|
||||
MockGet: func(_ context.Context, _ client.ObjectKey, obj client.Object) error {
|
||||
if meta.GetExternalName(obj.(metav1.Object)) == "test" {
|
||||
return errBoom
|
||||
}
|
||||
return nil
|
||||
},
|
||||
MockStatusUpdate: test.MockStatusUpdateFn(func(_ context.Context, obj client.Object, _ ...client.UpdateOption) error {
|
||||
want := &fake.Managed{}
|
||||
meta.SetExternalName(want, "test")
|
||||
want.SetConditions(xpv1.ReconcileError(errors.Wrap(errBoom, errUpdateManagedAfterCreate)))
|
||||
want.SetConditions(xpv1.Creating())
|
||||
if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" {
|
||||
reason := "Successful managed resource creation should be reported as a conditioned status."
|
||||
t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff)
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
},
|
||||
Scheme: fake.SchemeWith(&fake.Managed{}),
|
||||
},
|
||||
mg: resource.ManagedKind(fake.GVK(&fake.Managed{})),
|
||||
o: []ReconcilerOption{
|
||||
WithInitializers(),
|
||||
WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })),
|
||||
WithExternalConnecter(ExternalConnectorFn(func(_ context.Context, mg resource.Managed) (ExternalClient, error) {
|
||||
c := &ExternalClientFns{
|
||||
CreateFn: func(_ context.Context, mg resource.Managed) (ExternalCreation, error) {
|
||||
meta.SetExternalName(mg, "test")
|
||||
return ExternalCreation{ExternalNameAssigned: true}, nil
|
||||
},
|
||||
ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) {
|
||||
return ExternalObservation{}, nil
|
||||
},
|
||||
}
|
||||
return c, nil
|
||||
})),
|
||||
WithConnectionPublishers(),
|
||||
WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}),
|
||||
},
|
||||
},
|
||||
want: want{result: reconcile.Result{Requeue: true}},
|
||||
},
|
||||
"CreateWithExternalNameAssignmentUpdateError": {
|
||||
reason: "If the update after Create does not go through, we need to inform the user and requeue shortly.",
|
||||
args: args{
|
||||
m: &fake.Manager{
|
||||
Client: &test.MockClient{
|
||||
MockGet: test.NewMockGetFn(nil),
|
||||
MockUpdate: test.NewMockUpdateFn(errBoom),
|
||||
MockStatusUpdate: test.MockStatusUpdateFn(func(_ context.Context, obj client.Object, _ ...client.UpdateOption) error {
|
||||
want := &fake.Managed{}
|
||||
meta.SetExternalName(want, "test")
|
||||
want.SetConditions(xpv1.ReconcileError(errors.Wrap(errBoom, errUpdateManagedAfterCreate)))
|
||||
want.SetConditions(xpv1.Creating())
|
||||
if diff := cmp.Diff(want, obj, test.EquateConditions()); diff != "" {
|
||||
reason := "Successful managed resource creation should be reported as a conditioned status."
|
||||
t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff)
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
},
|
||||
Scheme: fake.SchemeWith(&fake.Managed{}),
|
||||
},
|
||||
mg: resource.ManagedKind(fake.GVK(&fake.Managed{})),
|
||||
o: []ReconcilerOption{
|
||||
WithInitializers(),
|
||||
WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })),
|
||||
WithExternalConnecter(ExternalConnectorFn(func(_ context.Context, mg resource.Managed) (ExternalClient, error) {
|
||||
c := &ExternalClientFns{
|
||||
CreateFn: func(_ context.Context, mg resource.Managed) (ExternalCreation, error) {
|
||||
meta.SetExternalName(mg, "test")
|
||||
return ExternalCreation{ExternalNameAssigned: true}, nil
|
||||
},
|
||||
ObserveFn: func(_ context.Context, _ resource.Managed) (ExternalObservation, error) {
|
||||
return ExternalObservation{}, nil
|
||||
},
|
||||
}
|
||||
return c, nil
|
||||
})),
|
||||
WithCriticalAnnotationUpdater(CriticalAnnotationUpdateFn(func(ctx context.Context, o client.Object) error { return nil })),
|
||||
WithConnectionPublishers(),
|
||||
WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}),
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue