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"
|
"fmt"
|
||||||
"hash/fnv"
|
"hash/fnv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
|
@ -31,9 +32,32 @@ import (
|
||||||
xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
|
xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AnnotationKeyExternalName is the key in the annotations map of a resource for
|
const (
|
||||||
// the name of the resource as it appears on provider's systems.
|
// AnnotationKeyExternalName is the key in the annotations map of a
|
||||||
const AnnotationKeyExternalName = "crossplane.io/external-name"
|
// 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
|
// 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
|
// 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})
|
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
|
// AllowPropagation from one object to another by adding consenting annotations
|
||||||
// to both.
|
// to both.
|
||||||
// Deprecated: This functionality will be removed soon.
|
// Deprecated: This functionality will be removed soon.
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash/fnv"
|
"hash/fnv"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/pkg/errors"
|
"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) {
|
func TestAllowPropagation(t *testing.T) {
|
||||||
fromns := "from-namespace"
|
fromns := "from-namespace"
|
||||||
from := "from-name"
|
from := "from-name"
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ import (
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
"k8s.io/client-go/util/retry"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
|
||||||
xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
|
xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
|
||||||
|
|
@ -35,6 +37,7 @@ const (
|
||||||
errUpdateManaged = "cannot update managed resource"
|
errUpdateManaged = "cannot update managed resource"
|
||||||
errUpdateManagedStatus = "cannot update managed resource status"
|
errUpdateManagedStatus = "cannot update managed resource status"
|
||||||
errResolveReferences = "cannot resolve references"
|
errResolveReferences = "cannot resolve references"
|
||||||
|
errUpdateCriticalAnnotations = "cannot update critical annotations"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NameAsExternalName writes the name of the managed resource to
|
// 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)
|
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)
|
r := NewAPISimpleReferenceResolver(tc.c)
|
||||||
got := r.ResolveReferences(tc.args.ctx, tc.args.mg)
|
got := r.ResolveReferences(tc.args.ctx, tc.args.mg)
|
||||||
if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" {
|
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"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/types"
|
|
||||||
"k8s.io/client-go/util/retry"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
|
@ -43,12 +40,14 @@ const (
|
||||||
reconcileTimeout = 1 * time.Minute
|
reconcileTimeout = 1 * time.Minute
|
||||||
|
|
||||||
defaultpollInterval = 1 * time.Minute
|
defaultpollInterval = 1 * time.Minute
|
||||||
|
defaultGracePeriod = 30 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
// Error strings.
|
// Error strings.
|
||||||
const (
|
const (
|
||||||
errGetManaged = "cannot get managed resource"
|
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"
|
errReconcileConnect = "connect failed"
|
||||||
errReconcileObserve = "observe failed"
|
errReconcileObserve = "observe failed"
|
||||||
errReconcileCreate = "create failed"
|
errReconcileCreate = "create failed"
|
||||||
|
|
@ -72,6 +71,7 @@ const (
|
||||||
reasonDeleted event.Reason = "DeletedExternalResource"
|
reasonDeleted event.Reason = "DeletedExternalResource"
|
||||||
reasonCreated event.Reason = "CreatedExternalResource"
|
reasonCreated event.Reason = "CreatedExternalResource"
|
||||||
reasonUpdated event.Reason = "UpdatedExternalResource"
|
reasonUpdated event.Reason = "UpdatedExternalResource"
|
||||||
|
reasonPending event.Reason = "PendingExternalResource"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ControllerName returns the recommended name for controllers that use this
|
// ControllerName returns the recommended name for controllers that use this
|
||||||
|
|
@ -80,6 +80,21 @@ func ControllerName(kind string) string {
|
||||||
return "managed/" + strings.ToLower(kind)
|
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
|
// ConnectionDetails created or updated during an operation on an external
|
||||||
// resource, for example usernames, passwords, endpoints, ports, etc.
|
// resource, for example usernames, passwords, endpoints, ports, etc.
|
||||||
type ConnectionDetails map[string][]byte
|
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
|
// 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.
|
// return error if there is an ongoing deletion or resource does not exist.
|
||||||
type ExternalClient interface {
|
type ExternalClient interface {
|
||||||
// Observe the external resource the supplied Managed resource represents,
|
// Observe the external resource the supplied Managed resource
|
||||||
// if any. Observe implementations must not modify the external resource,
|
// represents, if any. Observe implementations must not modify the
|
||||||
// but may update the supplied Managed resource to reflect the state of the
|
// external resource, but may update the supplied Managed resource to
|
||||||
// external resource.
|
// 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)
|
Observe(ctx context.Context, mg resource.Managed) (ExternalObservation, error)
|
||||||
|
|
||||||
// Create an external resource per the specifications of the supplied
|
// Create an external resource per the specifications of the supplied
|
||||||
// Managed resource. Called when Observe reports that the associated
|
// 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)
|
Create(ctx context.Context, mg resource.Managed) (ExternalCreation, error)
|
||||||
|
|
||||||
// Update the external resource represented by the supplied Managed
|
// 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.
|
// An ExternalCreation is the result of the creation of an external resource.
|
||||||
type ExternalCreation struct {
|
type ExternalCreation struct {
|
||||||
// ExternalNameAssigned is true if the Create operation resulted in a change
|
// ExternalNameAssigned should be true if the Create operation resulted
|
||||||
// in the external name annotation. If that's the case, we need to issue a
|
// in a change in the resource's external name. This is typically only
|
||||||
// spec update and make sure it goes through so that we don't lose the identifier
|
// needed for external resource's with unpredictable external names that
|
||||||
// of the resource we just created.
|
// 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
|
ExternalNameAssigned bool
|
||||||
|
|
||||||
// ConnectionDetails required to connect to this resource. These details
|
// ConnectionDetails required to connect to this resource. These details
|
||||||
|
|
@ -350,6 +373,7 @@ type Reconciler struct {
|
||||||
|
|
||||||
pollInterval time.Duration
|
pollInterval time.Duration
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
|
creationGracePeriod time.Duration
|
||||||
|
|
||||||
// The below structs embed the set of interfaces used to implement the
|
// The below structs embed the set of interfaces used to implement the
|
||||||
// managed resource reconciler. We do this primarily for readability, so
|
// managed resource reconciler. We do this primarily for readability, so
|
||||||
|
|
@ -363,6 +387,7 @@ type Reconciler struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type mrManaged struct {
|
type mrManaged struct {
|
||||||
|
CriticalAnnotationUpdater
|
||||||
ConnectionPublisher
|
ConnectionPublisher
|
||||||
resource.Finalizer
|
resource.Finalizer
|
||||||
Initializer
|
Initializer
|
||||||
|
|
@ -371,6 +396,7 @@ type mrManaged struct {
|
||||||
|
|
||||||
func defaultMRManaged(m manager.Manager) mrManaged {
|
func defaultMRManaged(m manager.Manager) mrManaged {
|
||||||
return mrManaged{
|
return mrManaged{
|
||||||
|
CriticalAnnotationUpdater: NewRetryingCriticalAnnotationUpdater(m.GetClient()),
|
||||||
ConnectionPublisher: NewAPISecretPublisher(m.GetClient(), m.GetScheme()),
|
ConnectionPublisher: NewAPISecretPublisher(m.GetClient(), m.GetScheme()),
|
||||||
Finalizer: resource.NewAPIFinalizer(m.GetClient(), managedFinalizerName),
|
Finalizer: resource.NewAPIFinalizer(m.GetClient(), managedFinalizerName),
|
||||||
Initializer: NewNameAsExternalName(m.GetClient()),
|
Initializer: NewNameAsExternalName(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
|
// WithExternalConnecter specifies how the Reconciler should connect to the API
|
||||||
// used to sync and delete external resources.
|
// used to sync and delete external resources.
|
||||||
func WithExternalConnecter(c ExternalConnecter) ReconcilerOption {
|
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
|
// WithConnectionPublishers specifies how the Reconciler should publish
|
||||||
// its connection details such as credentials and endpoints.
|
// its connection details such as credentials and endpoints.
|
||||||
func WithConnectionPublishers(p ...ConnectionPublisher) ReconcilerOption {
|
func WithConnectionPublishers(p ...ConnectionPublisher) ReconcilerOption {
|
||||||
|
|
@ -486,6 +533,7 @@ func NewReconciler(m manager.Manager, of resource.ManagedKind, o ...ReconcilerOp
|
||||||
client: m.GetClient(),
|
client: m.GetClient(),
|
||||||
newManaged: nm,
|
newManaged: nm,
|
||||||
pollInterval: defaultpollInterval,
|
pollInterval: defaultpollInterval,
|
||||||
|
creationGracePeriod: defaultGracePeriod,
|
||||||
timeout: reconcileTimeout,
|
timeout: reconcileTimeout,
|
||||||
managed: defaultMRManaged(m),
|
managed: defaultMRManaged(m),
|
||||||
external: defaultMRExternal(),
|
external: defaultMRExternal(),
|
||||||
|
|
@ -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)
|
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
|
// 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
|
// in some rare examples we need a spec field to make the observe call, and
|
||||||
// that spec field could be set by a reference.
|
// 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)
|
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) {
|
if meta.WasDeleted(managed) {
|
||||||
log = log.WithValues("deletion-timestamp", managed.GetDeletionTimestamp())
|
log = log.WithValues("deletion-timestamp", managed.GetDeletionTimestamp())
|
||||||
managed.SetConditions(xpv1.Deleting())
|
managed.SetConditions(xpv1.Deleting())
|
||||||
|
|
@ -711,6 +781,21 @@ func (r *Reconciler) Reconcile(_ context.Context, req reconcile.Request) (reconc
|
||||||
}
|
}
|
||||||
|
|
||||||
if !observation.ResourceExists {
|
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())
|
managed.SetConditions(xpv1.Creating())
|
||||||
creation, err := external.Create(externalCtx, managed)
|
creation, err := external.Create(externalCtx, managed)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -721,31 +806,50 @@ func (r *Reconciler) Reconcile(_ context.Context, req reconcile.Request) (reconc
|
||||||
// the new error condition. If not, we requeue explicitly, which will trigger backoff.
|
// the new error condition. If not, we requeue explicitly, which will trigger backoff.
|
||||||
log.Debug("Cannot create external resource", "error", err)
|
log.Debug("Cannot create external resource", "error", err)
|
||||||
record.Event(managed, event.Warning(reasonCannotCreate, 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)))
|
managed.SetConditions(xpv1.ReconcileError(errors.Wrap(err, errReconcileCreate)))
|
||||||
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
|
return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
if creation.ExternalNameAssigned {
|
// In some cases our external-name may be set by Create above.
|
||||||
en := meta.GetExternalName(managed)
|
log = log.WithValues("external-name", meta.GetExternalName(managed))
|
||||||
// We will retry in all cases where the error comes from the api-server.
|
record = r.record.WithAnnotations("external-name", meta.GetExternalName(managed))
|
||||||
// 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
|
// We handle annotations specially here because it's critical
|
||||||
// might be leaked.
|
// that they are persisted to the API server. If we don't remove
|
||||||
err := retry.OnError(retry.DefaultRetry, resource.IsAPIError, func() error {
|
// add the external-create-succeeded annotation the reconciler
|
||||||
nn := types.NamespacedName{Name: managed.GetName()}
|
// will refuse to proceed, because it won't know whether or not
|
||||||
if err := r.client.Get(ctx, nn, managed); err != nil {
|
// it created an external resource. This is also important in
|
||||||
return err
|
// cases where we must record an external-name annotation set by
|
||||||
}
|
// the Create call. Any other changes made during Create will be
|
||||||
meta.SetExternalName(managed, en)
|
// reverted when annotations are updated; at the time of writing
|
||||||
return r.client.Update(ctx, managed)
|
// Create implementations are advised not to alter status, but
|
||||||
})
|
// we may revisit this in future.
|
||||||
if err != nil {
|
meta.SetExternalCreateSucceeded(managed, time.Now())
|
||||||
log.Debug("Cannot update managed resource", "error", err)
|
if err := r.managed.UpdateCriticalAnnotations(ctx, managed); err != nil {
|
||||||
record.Event(managed, event.Warning(reasonCannotUpdateManaged, errors.Wrap(err, errUpdateManagedAfterCreate)))
|
log.Debug(errUpdateManagedAnnotations, "error", err)
|
||||||
managed.SetConditions(xpv1.ReconcileError(errors.Wrap(err, errUpdateManagedAfterCreate)))
|
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)
|
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 {
|
if err := r.managed.PublishConnection(ctx, managed, creation.ConnectionDetails); err != nil {
|
||||||
// If this is the first time we encounter this issue we'll be
|
// If this is the first time we encounter this issue we'll be
|
||||||
|
|
|
||||||
|
|
@ -19,12 +19,14 @@ package managed
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/manager"
|
"sigs.k8s.io/controller-runtime/pkg/manager"
|
||||||
|
|
@ -199,6 +201,35 @@ func TestReconciler(t *testing.T) {
|
||||||
},
|
},
|
||||||
want: want{result: reconcile.Result{Requeue: true}},
|
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": {
|
"ResolveReferencesError": {
|
||||||
reason: "Errors during reference resolution references should trigger a requeue after a short wait.",
|
reason: "Errors during reference resolution references should trigger a requeue after a short wait.",
|
||||||
args: args{
|
args: args{
|
||||||
|
|
@ -288,6 +319,34 @@ func TestReconciler(t *testing.T) {
|
||||||
},
|
},
|
||||||
want: want{result: reconcile.Result{Requeue: true}},
|
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": {
|
"ExternalDeleteError": {
|
||||||
reason: "Errors deleting the external resource should trigger a requeue after a short wait.",
|
reason: "Errors deleting the external resource should trigger a requeue after a short wait.",
|
||||||
args: args{
|
args: args{
|
||||||
|
|
@ -558,17 +617,18 @@ func TestReconciler(t *testing.T) {
|
||||||
},
|
},
|
||||||
want: want{result: reconcile.Result{Requeue: true}},
|
want: want{result: reconcile.Result{Requeue: true}},
|
||||||
},
|
},
|
||||||
"CreateExternalError": {
|
"UpdateCreatePendingError": {
|
||||||
reason: "Errors while creating an external resource should trigger a requeue after a short wait.",
|
reason: "Errors while updating our external-create-pending annotation should trigger a requeue after a short wait.",
|
||||||
args: args{
|
args: args{
|
||||||
m: &fake.Manager{
|
m: &fake.Manager{
|
||||||
Client: &test.MockClient{
|
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 {
|
MockStatusUpdate: test.MockStatusUpdateFn(func(_ context.Context, obj client.Object, _ ...client.UpdateOption) error {
|
||||||
want := &fake.Managed{}
|
want := &fake.Managed{}
|
||||||
want.SetConditions(xpv1.ReconcileError(errors.Wrap(errBoom, errReconcileCreate)))
|
meta.SetExternalCreatePending(want, time.Now())
|
||||||
want.SetConditions(xpv1.Creating())
|
want.SetConditions(xpv1.ReconcileError(errors.Wrap(errBoom, errUpdateManaged)))
|
||||||
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 while creating an external resource should be reported as a conditioned status."
|
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)
|
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}},
|
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": {
|
"PublishCreationConnectionDetailsError": {
|
||||||
reason: "Errors publishing connection details after creation should trigger a requeue after a short wait.",
|
reason: "Errors publishing connection details after creation should trigger a requeue after a short wait.",
|
||||||
args: args{
|
args: args{
|
||||||
m: &fake.Manager{
|
m: &fake.Manager{
|
||||||
Client: &test.MockClient{
|
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 {
|
MockStatusUpdate: test.MockStatusUpdateFn(func(_ context.Context, obj client.Object, _ ...client.UpdateOption) error {
|
||||||
want := &fake.Managed{}
|
want := &fake.Managed{}
|
||||||
|
meta.SetExternalCreatePending(want, time.Now())
|
||||||
|
meta.SetExternalCreateSucceeded(want, time.Now())
|
||||||
want.SetConditions(xpv1.ReconcileError(errBoom))
|
want.SetConditions(xpv1.ReconcileError(errBoom))
|
||||||
want.SetConditions(xpv1.Creating())
|
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."
|
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)
|
t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff)
|
||||||
}
|
}
|
||||||
|
|
@ -633,6 +785,7 @@ func TestReconciler(t *testing.T) {
|
||||||
}
|
}
|
||||||
return c, nil
|
return c, nil
|
||||||
})),
|
})),
|
||||||
|
WithCriticalAnnotationUpdater(CriticalAnnotationUpdateFn(func(ctx context.Context, o client.Object) error { return nil })),
|
||||||
WithConnectionPublishers(ConnectionPublisherFns{
|
WithConnectionPublishers(ConnectionPublisherFns{
|
||||||
PublishConnectionFn: func(_ context.Context, _ resource.Managed, cd ConnectionDetails) error {
|
PublishConnectionFn: func(_ context.Context, _ resource.Managed, cd ConnectionDetails) error {
|
||||||
// We're called after observe, create, and update
|
// We're called after observe, create, and update
|
||||||
|
|
@ -655,11 +808,14 @@ func TestReconciler(t *testing.T) {
|
||||||
m: &fake.Manager{
|
m: &fake.Manager{
|
||||||
Client: &test.MockClient{
|
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 {
|
MockStatusUpdate: test.MockStatusUpdateFn(func(_ context.Context, obj client.Object, _ ...client.UpdateOption) error {
|
||||||
want := &fake.Managed{}
|
want := &fake.Managed{}
|
||||||
|
meta.SetExternalCreatePending(want, time.Now())
|
||||||
|
meta.SetExternalCreateSucceeded(want, time.Now())
|
||||||
want.SetConditions(xpv1.ReconcileSuccess())
|
want.SetConditions(xpv1.ReconcileSuccess())
|
||||||
want.SetConditions(xpv1.Creating())
|
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."
|
reason := "Successful managed resource creation should be reported as a conditioned status."
|
||||||
t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff)
|
t.Errorf("\nReason: %s\n-want, +got:\n%s", reason, diff)
|
||||||
}
|
}
|
||||||
|
|
@ -673,139 +829,7 @@ func TestReconciler(t *testing.T) {
|
||||||
WithInitializers(),
|
WithInitializers(),
|
||||||
WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })),
|
WithReferenceResolver(ReferenceResolverFn(func(_ context.Context, _ resource.Managed) error { return nil })),
|
||||||
WithExternalConnecter(&NopConnecter{}),
|
WithExternalConnecter(&NopConnecter{}),
|
||||||
WithConnectionPublishers(),
|
WithCriticalAnnotationUpdater(CriticalAnnotationUpdateFn(func(ctx context.Context, o client.Object) error { return nil })),
|
||||||
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
|
|
||||||
})),
|
|
||||||
WithConnectionPublishers(),
|
WithConnectionPublishers(),
|
||||||
WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}),
|
WithFinalizer(resource.FinalizerFns{AddFinalizerFn: func(_ context.Context, _ resource.Object) error { return nil }}),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue