From cf744159e5f3a7f08fc325e4844f45f02430d4cd Mon Sep 17 00:00:00 2001 From: Nic Cope Date: Thu, 5 Nov 2020 04:12:20 +0000 Subject: [PATCH] Allow XRs and XRCs to record when they last published connection details This information is useful both for debugging XRs and XRCs, and for controllers to watch for updates to connection details (i.e. if those details were stored in an external system rather than in Secrets, which can themselves be watched). Signed-off-by: Nic Cope --- pkg/resource/fake/mocks.go | 18 +++++++++ pkg/resource/interfaces.go | 9 +++++ pkg/resource/unstructured/claim/claim.go | 14 +++++++ pkg/resource/unstructured/claim/claim_test.go | 37 +++++++++++++++++++ .../unstructured/composite/composite.go | 14 +++++++ .../unstructured/composite/composite_test.go | 37 +++++++++++++++++++ 6 files changed, 129 insertions(+) diff --git a/pkg/resource/fake/mocks.go b/pkg/resource/fake/mocks.go index e853fb0..da84cb9 100644 --- a/pkg/resource/fake/mocks.go +++ b/pkg/resource/fake/mocks.go @@ -181,6 +181,20 @@ func (m *ComposedResourcesReferencer) SetResourceReferences(r []corev1.ObjectRef // GetResourceReferences gets the composed references. func (m *ComposedResourcesReferencer) GetResourceReferences() []corev1.ObjectReference { return m.Refs } +// ConnectionDetailsLastPublishedTimer is a mock that implements the +// ConnectionDetailsLastPublishedTimer interface. +type ConnectionDetailsLastPublishedTimer struct{ Time *metav1.Time } + +// SetConnectionDetailsLastPublishedTime sets the published time. +func (c *ConnectionDetailsLastPublishedTimer) SetConnectionDetailsLastPublishedTime(t *metav1.Time) { + c.Time = t +} + +// GetConnectionDetailsLastPublishedTime gets the published time. +func (c *ConnectionDetailsLastPublishedTimer) GetConnectionDetailsLastPublishedTime() *metav1.Time { + return c.Time +} + // UserCounter is a mock that satisfies UserCounter // interface. type UserCounter struct{ Users int64 } @@ -251,7 +265,9 @@ type Composite struct { ComposedResourcesReferencer ClaimReferencer ConnectionSecretWriterTo + v1alpha1.ConditionedStatus + ConnectionDetailsLastPublishedTimer } // GetObjectKind returns schema.ObjectKind. @@ -300,7 +316,9 @@ type CompositeClaim struct { CompositionReferencer CompositeResourceReferencer LocalConnectionSecretWriterTo + v1alpha1.ConditionedStatus + ConnectionDetailsLastPublishedTimer } // GetObjectKind returns schema.ObjectKind. diff --git a/pkg/resource/interfaces.go b/pkg/resource/interfaces.go index 71846ad..169d33d 100644 --- a/pkg/resource/interfaces.go +++ b/pkg/resource/interfaces.go @@ -126,6 +126,13 @@ type UserCounter interface { GetUsers() int64 } +// A ConnectionDetailsPublishedTimer can record the last time its connection +// details were published. +type ConnectionDetailsPublishedTimer interface { + SetConnectionDetailsLastPublishedTime(t *metav1.Time) + GetConnectionDetailsLastPublishedTime() *metav1.Time +} + // An Object is a Kubernetes object. type Object interface { metav1.Object @@ -188,6 +195,7 @@ type Composite interface { ConnectionSecretWriterTo Conditioned + ConnectionDetailsPublishedTimer } // Composed resources can be a composed into a Composite resource. @@ -208,4 +216,5 @@ type CompositeClaim interface { LocalConnectionSecretWriterTo Conditioned + ConnectionDetailsPublishedTimer } diff --git a/pkg/resource/unstructured/claim/claim.go b/pkg/resource/unstructured/claim/claim.go index 80fa740..8fc8df9 100644 --- a/pkg/resource/unstructured/claim/claim.go +++ b/pkg/resource/unstructured/claim/claim.go @@ -139,3 +139,17 @@ func (c *Unstructured) SetConditions(conditions ...v1alpha1.Condition) { conditioned.SetConditions(conditions...) _ = fieldpath.Pave(c.Object).SetValue("status.conditions", conditioned.Conditions) } + +// GetConnectionDetailsLastPublishedTime of this composite resource claim. +func (c *Unstructured) GetConnectionDetailsLastPublishedTime() *metav1.Time { + out := &metav1.Time{} + if err := fieldpath.Pave(c.Object).GetValueInto("status.connectionDetails.lastPublishedTime", out); err != nil { + return nil + } + return out +} + +// SetConnectionDetailsLastPublishedTime of this composite resource claim. +func (c *Unstructured) SetConnectionDetailsLastPublishedTime(t *metav1.Time) { + _ = fieldpath.Pave(c.Object).SetValue("status.connectionDetails.lastPublishedTime", t) +} diff --git a/pkg/resource/unstructured/claim/claim_test.go b/pkg/resource/unstructured/claim/claim_test.go index d801381..2db0a56 100644 --- a/pkg/resource/unstructured/claim/claim_test.go +++ b/pkg/resource/unstructured/claim/claim_test.go @@ -17,7 +17,9 @@ limitations under the License. package claim import ( + "encoding/json" "testing" + "time" "github.com/google/go-cmp/cmp" corev1 "k8s.io/api/core/v1" @@ -213,3 +215,38 @@ func TestWriteConnectionSecretToReference(t *testing.T) { }) } } + +func TestConnectionDetailsLastPublishedTime(t *testing.T) { + now := &metav1.Time{Time: time.Now()} + + // The timestamp loses a little resolution when round-tripped through JSON + // encoding. + lores := func(t *metav1.Time) *metav1.Time { + out := &metav1.Time{} + j, _ := json.Marshal(t) + _ = json.Unmarshal(j, out) + return out + } + + cases := map[string]struct { + u *Unstructured + set *metav1.Time + want *metav1.Time + }{ + "NewTime": { + u: New(), + set: now, + want: lores(now), + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + tc.u.SetConnectionDetailsLastPublishedTime(tc.set) + got := tc.u.GetConnectionDetailsLastPublishedTime() + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("\nu.GetConnectionDetailsLastPublishedTime(): -want, +got:\n%s", diff) + } + }) + } +} diff --git a/pkg/resource/unstructured/composite/composite.go b/pkg/resource/unstructured/composite/composite.go index 3172fd6..bb1b0e0 100644 --- a/pkg/resource/unstructured/composite/composite.go +++ b/pkg/resource/unstructured/composite/composite.go @@ -161,3 +161,17 @@ func (c *Unstructured) SetConditions(conditions ...v1alpha1.Condition) { conditioned.SetConditions(conditions...) _ = fieldpath.Pave(c.Object).SetValue("status.conditions", conditioned.Conditions) } + +// GetConnectionDetailsLastPublishedTime of this Composite resource. +func (c *Unstructured) GetConnectionDetailsLastPublishedTime() *metav1.Time { + out := &metav1.Time{} + if err := fieldpath.Pave(c.Object).GetValueInto("status.connectionDetails.lastPublishedTime", out); err != nil { + return nil + } + return out +} + +// SetConnectionDetailsLastPublishedTime of this Composite resource. +func (c *Unstructured) SetConnectionDetailsLastPublishedTime(t *metav1.Time) { + _ = fieldpath.Pave(c.Object).SetValue("status.connectionDetails.lastPublishedTime", t) +} diff --git a/pkg/resource/unstructured/composite/composite_test.go b/pkg/resource/unstructured/composite/composite_test.go index 7771da4..cec197d 100644 --- a/pkg/resource/unstructured/composite/composite_test.go +++ b/pkg/resource/unstructured/composite/composite_test.go @@ -17,7 +17,9 @@ limitations under the License. package composite import ( + "encoding/json" "testing" + "time" "github.com/google/go-cmp/cmp" corev1 "k8s.io/api/core/v1" @@ -238,3 +240,38 @@ func TestWriteConnectionSecretToReference(t *testing.T) { }) } } + +func TestConnectionDetailsLastPublishedTime(t *testing.T) { + now := &metav1.Time{Time: time.Now()} + + // The timestamp loses a little resolution when round-tripped through JSON + // encoding. + lores := func(t *metav1.Time) *metav1.Time { + out := &metav1.Time{} + j, _ := json.Marshal(t) + _ = json.Unmarshal(j, out) + return out + } + + cases := map[string]struct { + u *Unstructured + set *metav1.Time + want *metav1.Time + }{ + "NewTime": { + u: New(), + set: now, + want: lores(now), + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + tc.u.SetConnectionDetailsLastPublishedTime(tc.set) + got := tc.u.GetConnectionDetailsLastPublishedTime() + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("\nu.GetConnectionDetailsLastPublishedTime(): -want, +got:\n%s", diff) + } + }) + } +}