Skip to content

Commit

Permalink
Add utilities for tracking external creation state as annotations
Browse files Browse the repository at this point in the history
This allows us to have a better idea of what happened during a prior reconcile
where we tried to create an external resource. This is particularly critical for
resources that return non-deterministic information (like an external name) that
we must persist in order to make the right decisions on future reconciles.

Signed-off-by: Nic Cope <negz@rk0n.org>
  • Loading branch information
negz committed Aug 31, 2021
1 parent 943a0be commit 171ba4d
Show file tree
Hide file tree
Showing 2 changed files with 249 additions and 23 deletions.
89 changes: 74 additions & 15 deletions pkg/meta/meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,26 @@ const (
// systems.
AnnotationKeyExternalName = "crossplane.io/external-name"

// AnnotationKeyExternalCreateTime 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
// AnnotationKeyExternalCreatePending is the key in the annotations map
// of a resource that indicates the last time creation of the external
// resource was pending. Its value must be an RFC3999 timestamp. This
// annotation is removed once we record the result of external resource
// creation.
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.
AnnotationKeyExternalCreateTime = "crossplane.io/external-create-time"
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
Expand Down Expand Up @@ -258,21 +270,68 @@ func SetExternalName(o metav1.Object, name string) {
AddAnnotations(o, map[string]string{AnnotationKeyExternalName: name})
}

// GetExternalCreateTime returns the time at which the external resource was
// most recently created.
func GetExternalCreateTime(o metav1.Object) *metav1.Time {
a := o.GetAnnotations()[AnnotationKeyExternalCreateTime]
// GetExternalCreatePending returns the time at which the external resource
// was most recently pending creation.
func GetExternalCreatePending(o metav1.Object) *metav1.Time {
a := o.GetAnnotations()[AnnotationKeyExternalCreatePending]
t, err := time.Parse(time.RFC3339, a)
if err != nil {
return nil
}
return &metav1.Time{Time: 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 metav1.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) *metav1.Time {
a := o.GetAnnotations()[AnnotationKeyExternalCreateSucceeded]
t, err := time.Parse(time.RFC3339, a)
if err != nil {
return nil
}
return &metav1.Time{Time: t}
}

// SetExternalCreateTime sets the time at which the external resource was most
// recently created to the supplied time.
func SetExternalCreateTime(o metav1.Object, t metav1.Time) {
AddAnnotations(o, map[string]string{AnnotationKeyExternalCreateTime: t.Format(time.RFC3339)})
// SetExternalCreateSucceeded sets the time at which the external resource was
// most recently created to the supplied time.
func SetExternalCreateSucceeded(o metav1.Object, t metav1.Time) {
AddAnnotations(o, map[string]string{AnnotationKeyExternalCreateSucceeded: t.Format(time.RFC3339)})
RemoveAnnotations(o, AnnotationKeyExternalCreatePending)
}

// GetExternalCreateFailed returns the time at which the external resource
// recently failed to create.
func GetExternalCreateFailed(o metav1.Object) *metav1.Time {
a := o.GetAnnotations()[AnnotationKeyExternalCreateFailed]
t, err := time.Parse(time.RFC3339, a)
if err != nil {
return nil
}
return &metav1.Time{Time: t}
}

// SetExternalCreateFailed sets the time at which the external resource most
// recently failed to create.
func SetExternalCreateFailed(o metav1.Object, t metav1.Time) {
AddAnnotations(o, map[string]string{AnnotationKeyExternalCreateFailed: t.Format(time.RFC3339)})
RemoveAnnotations(o, AnnotationKeyExternalCreatePending)
}

// 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 == nil {
return false
}
return time.Since(t.Time) < d
}

// AllowPropagation from one object to another by adding consenting annotations
Expand Down
183 changes: 175 additions & 8 deletions pkg/meta/meta_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -902,15 +902,67 @@ func TestSetExternalName(t *testing.T) {
}
}

func TestGetExternalCreateTime(t *testing.T) {
func TestGetExternalCreatePending(t *testing.T) {
now := &metav1.Time{Time: time.Now().Round(time.Second)}

cases := map[string]struct {
o metav1.Object
want *metav1.Time
}{
"ExternalCreatePendingExists": {
o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{AnnotationKeyExternalCreatePending: now.Format(time.RFC3339)}}},
want: now,
},
"NoExternalCreatePending": {
o: &corev1.Pod{},
want: nil,
},
}

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 := metav1.Now()

cases := map[string]struct {
o metav1.Object
t metav1.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 := &metav1.Time{Time: time.Now().Round(time.Second)}

cases := map[string]struct {
o metav1.Object
want *metav1.Time
}{
"ExternalCreateTimeExists": {
o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{AnnotationKeyExternalCreateTime: now.Format(time.RFC3339)}}},
o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{AnnotationKeyExternalCreateSucceeded: now.Format(time.RFC3339)}}},
want: now,
},
"NoExternalCreateTime": {
Expand All @@ -921,15 +973,15 @@ func TestGetExternalCreateTime(t *testing.T) {

for name, tc := range cases {
t.Run(name, func(t *testing.T) {
got := GetExternalCreateTime(tc.o)
got := GetExternalCreateSucceeded(tc.o)
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("GetExternalCreateTime(...): -want, +got:\n%s", diff)
t.Errorf("GetExternalCreateSucceeded(...): -want, +got:\n%s", diff)
}
})
}
}

func TestSetExternalCreateTime(t *testing.T) {
func TestSetExternalCreateSucceeded(t *testing.T) {
now := metav1.Now()

cases := map[string]struct {
Expand All @@ -940,15 +992,130 @@ func TestSetExternalCreateTime(t *testing.T) {
"SetsTheCorrectKey": {
o: &corev1.Pod{},
t: now,
want: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{AnnotationKeyExternalCreateTime: now.Format(time.RFC3339)}}},
want: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{AnnotationKeyExternalCreateSucceeded: now.Format(time.RFC3339)}}},
},
"RemovesCreatePendingKey": {
o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{AnnotationKeyExternalCreatePending: now.Format(time.RFC3339)}}},
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) {
SetExternalCreateTime(tc.o, tc.t)
SetExternalCreateSucceeded(tc.o, tc.t)
if diff := cmp.Diff(tc.want, tc.o); diff != "" {
t.Errorf("SetExternalCreateTime(...): -want, +got:\n%s", diff)
t.Errorf("SetExternalCreateSucceeded(...): -want, +got:\n%s", diff)
}
})
}
}

func TestGetExternalCreateFailed(t *testing.T) {
now := &metav1.Time{Time: time.Now().Round(time.Second)}

cases := map[string]struct {
o metav1.Object
want *metav1.Time
}{
"ExternalCreateFailedExists": {
o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{AnnotationKeyExternalCreateFailed: now.Format(time.RFC3339)}}},
want: now,
},
"NoExternalCreateFailed": {
o: &corev1.Pod{},
want: nil,
},
}

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 := metav1.Now()

cases := map[string]struct {
o metav1.Object
t metav1.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)}}},
},
"RemovesCreatePendingKey": {
o: &corev1.Pod{ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{AnnotationKeyExternalCreatePending: now.Format(time.RFC3339)}}},
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, metav1.NewTime(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, metav1.NewTime(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)
}
})
}
Expand Down

0 comments on commit 171ba4d

Please sign in to comment.