From 8a33531632594c4d3d9024e384a7ba7edd6d6f6e Mon Sep 17 00:00:00 2001 From: Predrag Knezevic Date: Wed, 27 Dec 2023 16:55:40 +0100 Subject: [PATCH] Add ability to expose resource reconciliation progress The following optional fields has been added to claim/composite/composed status * `observedGeneration` when available shows what is the last observed resource generation * `observedLabels` when available shows what are the last observed resource labels * `observedAnnotations` when available shows what are the last observed resource annotations Signed-off-by: Predrag Knezevic --- apis/common/v1/condition.go | 68 +++++++++++++++++++ apis/common/v1/zz_generated.deepcopy.go | 19 ++++++ pkg/resource/interfaces.go | 15 ++++ pkg/resource/interfaces_test.go | 11 ++- pkg/resource/unstructured/claim/claim.go | 45 ++++++++++++ pkg/resource/unstructured/claim/claim_test.go | 63 +++++++++++++++++ .../unstructured/composed/composed.go | 45 ++++++++++++ .../unstructured/composed/composed_test.go | 63 +++++++++++++++++ .../unstructured/composite/composite.go | 45 ++++++++++++ .../unstructured/composite/composite_test.go | 63 +++++++++++++++++ 10 files changed, 436 insertions(+), 1 deletion(-) diff --git a/apis/common/v1/condition.go b/apis/common/v1/condition.go index 3ee7dc5b9..8c9073ccc 100644 --- a/apis/common/v1/condition.go +++ b/apis/common/v1/condition.go @@ -107,6 +107,21 @@ type ConditionedStatus struct { // +listMapKey=type // +optional Conditions []Condition `json:"conditions,omitempty"` + + // ObservedGeneration is the most recent resource metadata.generation + // observed by the reconciler + // +optional + ObservedGeneration *int64 `json:"observedGeneration,omitempty"` + + // ObservedLabels are the most recent resource metadata.labels + // observed by the reconciler + // +optional + ObserverLabels map[string]string `json:"observedLabels,omitempty"` + + // ObservedLabels are the most recent resource metadata.annotations + // observed by the reconciler + // +optional + ObserverAnnotations map[string]string `json:"observedAnnotations,omitempty"` } // NewConditionedStatus returns a stat with the supplied conditions set. @@ -153,6 +168,59 @@ func (s *ConditionedStatus) SetConditions(c ...Condition) { } } +// SetObservedGeneration sets the generation of the main resource +// during the last reconciliation. +func (s *ConditionedStatus) SetObservedGeneration(generation int64) { + s.ObservedGeneration = &generation +} + +// GetObservedGeneration returns the last observed generation of the main resource. +func (s *ConditionedStatus) GetObservedGeneration() *int64 { + return s.ObservedGeneration +} + +// SetObservedLabels set the labels observed on the main resource +// during the last reconciliation. +func (s *ConditionedStatus) SetObservedLabels(labels map[string]string) { + s.ObserverLabels = make(map[string]string) + for k, v := range labels { + s.ObserverLabels[k] = v + } +} + +// GetObservedLabels returns the last observed labels of the main resource. +func (s *ConditionedStatus) GetObservedLabels() map[string]string { + if s.ObserverLabels == nil { + return nil + } + r := make(map[string]string) + for k, v := range s.ObserverLabels { + r[k] = v + } + return r +} + +// SetObservedAnnotations set the annotations observed on the main resource +// during the last reconciliation. +func (s *ConditionedStatus) SetObservedAnnotations(annotations map[string]string) { + s.ObserverAnnotations = make(map[string]string) + for k, v := range annotations { + s.ObserverAnnotations[k] = v + } +} + +// GetObservedAnnotations returns the last observed annotations of the main resource. +func (s *ConditionedStatus) GetObservedAnnotations() map[string]string { + if s.ObserverAnnotations == nil { + return nil + } + r := make(map[string]string) + for k, v := range s.ObserverAnnotations { + r[k] = v + } + return r +} + // Equal returns true if the status is identical to the supplied status, // ignoring the LastTransitionTimes and order of statuses. func (s *ConditionedStatus) Equal(other *ConditionedStatus) bool { diff --git a/apis/common/v1/zz_generated.deepcopy.go b/apis/common/v1/zz_generated.deepcopy.go index 4d9e8a902..f9b0f1a99 100644 --- a/apis/common/v1/zz_generated.deepcopy.go +++ b/apis/common/v1/zz_generated.deepcopy.go @@ -80,6 +80,25 @@ func (in *ConditionedStatus) DeepCopyInto(out *ConditionedStatus) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.ObservedGeneration != nil { + in, out := &in.ObservedGeneration, &out.ObservedGeneration + *out = new(int64) + **out = **in + } + if in.ObserverLabels != nil { + in, out := &in.ObserverLabels, &out.ObserverLabels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.ObserverAnnotations != nil { + in, out := &in.ObserverAnnotations, &out.ObserverAnnotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConditionedStatus. diff --git a/pkg/resource/interfaces.go b/pkg/resource/interfaces.go index a0a0603fa..d5fa94892 100644 --- a/pkg/resource/interfaces.go +++ b/pkg/resource/interfaces.go @@ -176,6 +176,18 @@ type ConnectionDetailsPublishedTimer interface { GetConnectionDetailsLastPublishedTime() *metav1.Time } +// ReconciliationObserver can track data observed by resource reconciler. +type ReconciliationObserver interface { + SetObservedGeneration(generation int64) + GetObservedGeneration() *int64 + + SetObservedLabels(labels map[string]string) + GetObservedLabels() map[string]string + + SetObservedAnnotations(annotations map[string]string) + GetObservedAnnotations() map[string]string +} + // An Object is a Kubernetes object. type Object interface { metav1.Object @@ -245,6 +257,7 @@ type Composite interface { Conditioned ConnectionDetailsPublishedTimer + ReconciliationObserver } // Composed resources can be a composed into a Composite resource. @@ -254,6 +267,7 @@ type Composed interface { Conditioned ConnectionSecretWriterTo ConnectionDetailsPublisherTo + ReconciliationObserver } // A CompositeClaim for a Composite resource. @@ -272,4 +286,5 @@ type CompositeClaim interface { Conditioned ConnectionDetailsPublishedTimer + ReconciliationObserver } diff --git a/pkg/resource/interfaces_test.go b/pkg/resource/interfaces_test.go index 1617cbf49..eea3cd267 100644 --- a/pkg/resource/interfaces_test.go +++ b/pkg/resource/interfaces_test.go @@ -16,7 +16,12 @@ limitations under the License. package resource -import "github.com/crossplane/crossplane-runtime/pkg/resource/fake" +import ( + "github.com/crossplane/crossplane-runtime/pkg/resource/fake" + "github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/claim" + "github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/composed" + "github.com/crossplane/crossplane-runtime/pkg/resource/unstructured/composite" +) // We test that our fakes satisfy our interfaces here rather than in the fake // package to avoid a cyclic dependency. @@ -29,4 +34,8 @@ var ( _ CompositeClaim = &fake.CompositeClaim{} _ Composite = &fake.Composite{} _ Composed = &fake.Composed{} + + _ CompositeClaim = &claim.Unstructured{} + _ Composite = &composite.Unstructured{} + _ Composed = &composed.Unstructured{} ) diff --git a/pkg/resource/unstructured/claim/claim.go b/pkg/resource/unstructured/claim/claim.go index 486a2afa7..877a1b576 100644 --- a/pkg/resource/unstructured/claim/claim.go +++ b/pkg/resource/unstructured/claim/claim.go @@ -253,3 +253,48 @@ func (c *Unstructured) GetConnectionDetailsLastPublishedTime() *metav1.Time { func (c *Unstructured) SetConnectionDetailsLastPublishedTime(t *metav1.Time) { _ = fieldpath.Pave(c.Object).SetValue("status.connectionDetails.lastPublishedTime", t) } + +// SetObservedGeneration of this composite resource claim. +func (c *Unstructured) SetObservedGeneration(generation int64) { + status := &xpv1.ConditionedStatus{} + _ = fieldpath.Pave(c.Object).GetValueInto("status", status) + status.SetObservedGeneration(generation) + _ = fieldpath.Pave(c.Object).SetValue("status", status) +} + +// GetObservedGeneration of this composite resource claim. +func (c *Unstructured) GetObservedGeneration() *int64 { + status := &xpv1.ConditionedStatus{} + _ = fieldpath.Pave(c.Object).GetValueInto("status", status) + return status.GetObservedGeneration() +} + +// SetObservedLabels of this composite resource claim. +func (c *Unstructured) SetObservedLabels(labels map[string]string) { + status := &xpv1.ConditionedStatus{} + _ = fieldpath.Pave(c.Object).GetValueInto("status", status) + status.SetObservedLabels(labels) + _ = fieldpath.Pave(c.Object).SetValue("status", status) +} + +// GetObservedLabels of this composite resource claim. +func (c *Unstructured) GetObservedLabels() map[string]string { + status := &xpv1.ConditionedStatus{} + _ = fieldpath.Pave(c.Object).GetValueInto("status", status) + return status.GetObservedLabels() +} + +// SetObservedAnnotations of this composite resource claim. +func (c *Unstructured) SetObservedAnnotations(annotations map[string]string) { + status := &xpv1.ConditionedStatus{} + _ = fieldpath.Pave(c.Object).GetValueInto("status", status) + status.SetObservedAnnotations(annotations) + _ = fieldpath.Pave(c.Object).SetValue("status", status) +} + +// GetObservedAnnotations of this composite resource claim. +func (c *Unstructured) GetObservedAnnotations() map[string]string { + status := &xpv1.ConditionedStatus{} + _ = fieldpath.Pave(c.Object).GetValueInto("status", status) + return status.GetObservedAnnotations() +} diff --git a/pkg/resource/unstructured/claim/claim_test.go b/pkg/resource/unstructured/claim/claim_test.go index 55262c090..141083cf6 100644 --- a/pkg/resource/unstructured/claim/claim_test.go +++ b/pkg/resource/unstructured/claim/claim_test.go @@ -368,3 +368,66 @@ func TestConnectionDetailsLastPublishedTime(t *testing.T) { }) } } + +func TestObservedGeneration(t *testing.T) { + u := &Unstructured{unstructured.Unstructured{Object: map[string]any{}}} + want := int64(123) + u.SetObservedGeneration(want) + + if got := u.GetObservedGeneration(); *got != want { + t.Errorf("u.GetObservedGeneration() got: %v, want %v", *got, want) + } + if g := u.GetUnstructured().Object["status"].(map[string]any)["observedGeneration"]; g != want { + t.Errorf("Generations do not match! got: %v (%T)", g, g) + } +} + +func TestObservedGenerationNotFound(t *testing.T) { + u := &Unstructured{unstructured.Unstructured{Object: map[string]any{}}} + + if g := u.GetObservedGeneration(); g != nil { + t.Errorf("u.GetObservedGeneration(): expected nil, but got %v", g) + } +} + +func TestObservedLabels(t *testing.T) { + u := &Unstructured{unstructured.Unstructured{Object: map[string]any{}}} + labels := map[string]string{ + "foo": "1", + "bar": "2", + } + u.SetObservedLabels(labels) + + if diff := cmp.Diff(labels, u.GetObservedLabels()); diff != "" { + t.Errorf("u.GetObservedLabels(): -want, +got:\n%s", diff) + } +} + +func TestObservedLabelsNotFound(t *testing.T) { + u := &Unstructured{unstructured.Unstructured{Object: map[string]any{}}} + + if g := u.GetObservedLabels(); g != nil { + t.Errorf("u.GetObservedLabels(): expected nil, but got %v", g) + } +} + +func TestObservedAnnotations(t *testing.T) { + u := &Unstructured{unstructured.Unstructured{Object: map[string]any{}}} + annotations := map[string]string{ + "foo": "1", + "bar": "2", + } + u.SetObservedAnnotations(annotations) + + if diff := cmp.Diff(annotations, u.GetObservedAnnotations()); diff != "" { + t.Errorf("u.GetObservedAnnotations(): -want, +got:\n%s", diff) + } +} + +func TestObservedAnnotationsNotFound(t *testing.T) { + u := &Unstructured{unstructured.Unstructured{Object: map[string]any{}}} + + if g := u.GetObservedAnnotations(); g != nil { + t.Errorf("u.GetObservedAnnotations(): expected nil, but got %v", g) + } +} diff --git a/pkg/resource/unstructured/composed/composed.go b/pkg/resource/unstructured/composed/composed.go index 2478650eb..1552a1859 100644 --- a/pkg/resource/unstructured/composed/composed.go +++ b/pkg/resource/unstructured/composed/composed.go @@ -168,3 +168,48 @@ type UnstructuredList struct { func (cr *UnstructuredList) GetUnstructuredList() *unstructured.UnstructuredList { return &cr.UnstructuredList } + +// SetObservedGeneration of this composite resource claim. +func (cr *Unstructured) SetObservedGeneration(generation int64) { + status := &xpv1.ConditionedStatus{} + _ = fieldpath.Pave(cr.Object).GetValueInto("status", status) + status.SetObservedGeneration(generation) + _ = fieldpath.Pave(cr.Object).SetValue("status", status) +} + +// GetObservedGeneration of this composite resource claim. +func (cr *Unstructured) GetObservedGeneration() *int64 { + status := &xpv1.ConditionedStatus{} + _ = fieldpath.Pave(cr.Object).GetValueInto("status", status) + return status.GetObservedGeneration() +} + +// SetObservedLabels of this composite resource claim. +func (cr *Unstructured) SetObservedLabels(labels map[string]string) { + status := &xpv1.ConditionedStatus{} + _ = fieldpath.Pave(cr.Object).GetValueInto("status", status) + status.SetObservedLabels(labels) + _ = fieldpath.Pave(cr.Object).SetValue("status", status) +} + +// GetObservedLabels of this composite resource claim. +func (cr *Unstructured) GetObservedLabels() map[string]string { + status := &xpv1.ConditionedStatus{} + _ = fieldpath.Pave(cr.Object).GetValueInto("status", status) + return status.GetObservedLabels() +} + +// SetObservedAnnotations of this composite resource claim. +func (cr *Unstructured) SetObservedAnnotations(annotations map[string]string) { + status := &xpv1.ConditionedStatus{} + _ = fieldpath.Pave(cr.Object).GetValueInto("status", status) + status.SetObservedAnnotations(annotations) + _ = fieldpath.Pave(cr.Object).SetValue("status", status) +} + +// GetObservedAnnotations of this composite resource claim. +func (cr *Unstructured) GetObservedAnnotations() map[string]string { + status := &xpv1.ConditionedStatus{} + _ = fieldpath.Pave(cr.Object).GetValueInto("status", status) + return status.GetObservedAnnotations() +} diff --git a/pkg/resource/unstructured/composed/composed_test.go b/pkg/resource/unstructured/composed/composed_test.go index 5e3afb848..b5ef60d5e 100644 --- a/pkg/resource/unstructured/composed/composed_test.go +++ b/pkg/resource/unstructured/composed/composed_test.go @@ -147,3 +147,66 @@ func TestWriteConnectionSecretToReference(t *testing.T) { }) } } + +func TestObservedGeneration(t *testing.T) { + u := &Unstructured{unstructured.Unstructured{Object: map[string]any{}}} + want := int64(123) + u.SetObservedGeneration(want) + + if got := u.GetObservedGeneration(); *got != want { + t.Errorf("u.GetObservedGeneration() got: %v, want %v", *got, want) + } + if g := u.GetUnstructured().Object["status"].(map[string]any)["observedGeneration"]; g != want { + t.Errorf("Generations do not match! got: %v (%T)", g, g) + } +} + +func TestObservedGenerationNotFound(t *testing.T) { + u := &Unstructured{unstructured.Unstructured{Object: map[string]any{}}} + + if g := u.GetObservedGeneration(); g != nil { + t.Errorf("u.GetObservedGeneration(): expected nil, but got %v", g) + } +} + +func TestObservedLabels(t *testing.T) { + u := &Unstructured{unstructured.Unstructured{Object: map[string]any{}}} + labels := map[string]string{ + "foo": "1", + "bar": "2", + } + u.SetObservedLabels(labels) + + if diff := cmp.Diff(labels, u.GetObservedLabels()); diff != "" { + t.Errorf("u.GetObservedLabels(): -want, +got:\n%s", diff) + } +} + +func TestObservedLabelsNotFound(t *testing.T) { + u := &Unstructured{unstructured.Unstructured{Object: map[string]any{}}} + + if g := u.GetObservedLabels(); g != nil { + t.Errorf("u.GetObservedLabels(): expected nil, but got %v", g) + } +} + +func TestObservedAnnotations(t *testing.T) { + u := &Unstructured{unstructured.Unstructured{Object: map[string]any{}}} + annotations := map[string]string{ + "foo": "1", + "bar": "2", + } + u.SetObservedAnnotations(annotations) + + if diff := cmp.Diff(annotations, u.GetObservedAnnotations()); diff != "" { + t.Errorf("u.GetObservedAnnotations(): -want, +got:\n%s", diff) + } +} + +func TestObservedAnnotationsNotFound(t *testing.T) { + u := &Unstructured{unstructured.Unstructured{Object: map[string]any{}}} + + if g := u.GetObservedAnnotations(); g != nil { + t.Errorf("u.GetObservedAnnotations(): expected nil, but got %v", g) + } +} diff --git a/pkg/resource/unstructured/composite/composite.go b/pkg/resource/unstructured/composite/composite.go index 1e65972d5..3f4d71bce 100644 --- a/pkg/resource/unstructured/composite/composite.go +++ b/pkg/resource/unstructured/composite/composite.go @@ -258,3 +258,48 @@ func (c *Unstructured) SetEnvironmentConfigReferences(refs []corev1.ObjectRefere } _ = fieldpath.Pave(c.Object).SetValue("spec.environmentConfigRefs", filtered) } + +// SetObservedGeneration of this composite resource claim. +func (c *Unstructured) SetObservedGeneration(generation int64) { + status := &xpv1.ConditionedStatus{} + _ = fieldpath.Pave(c.Object).GetValueInto("status", status) + status.SetObservedGeneration(generation) + _ = fieldpath.Pave(c.Object).SetValue("status", status) +} + +// GetObservedGeneration of this composite resource claim. +func (c *Unstructured) GetObservedGeneration() *int64 { + status := &xpv1.ConditionedStatus{} + _ = fieldpath.Pave(c.Object).GetValueInto("status", status) + return status.GetObservedGeneration() +} + +// SetObservedLabels of this composite resource claim. +func (c *Unstructured) SetObservedLabels(labels map[string]string) { + status := &xpv1.ConditionedStatus{} + _ = fieldpath.Pave(c.Object).GetValueInto("status", status) + status.SetObservedLabels(labels) + _ = fieldpath.Pave(c.Object).SetValue("status", status) +} + +// GetObservedLabels of this composite resource claim. +func (c *Unstructured) GetObservedLabels() map[string]string { + status := &xpv1.ConditionedStatus{} + _ = fieldpath.Pave(c.Object).GetValueInto("status", status) + return status.GetObservedLabels() +} + +// SetObservedAnnotations of this composite resource claim. +func (c *Unstructured) SetObservedAnnotations(annotations map[string]string) { + status := &xpv1.ConditionedStatus{} + _ = fieldpath.Pave(c.Object).GetValueInto("status", status) + status.SetObservedAnnotations(annotations) + _ = fieldpath.Pave(c.Object).SetValue("status", status) +} + +// GetObservedAnnotations of this composite resource claim. +func (c *Unstructured) GetObservedAnnotations() map[string]string { + status := &xpv1.ConditionedStatus{} + _ = fieldpath.Pave(c.Object).GetValueInto("status", status) + return status.GetObservedAnnotations() +} diff --git a/pkg/resource/unstructured/composite/composite_test.go b/pkg/resource/unstructured/composite/composite_test.go index c808651ab..875ec7f52 100644 --- a/pkg/resource/unstructured/composite/composite_test.go +++ b/pkg/resource/unstructured/composite/composite_test.go @@ -355,3 +355,66 @@ func TestConnectionDetailsLastPublishedTime(t *testing.T) { }) } } + +func TestObservedGeneration(t *testing.T) { + u := &Unstructured{unstructured.Unstructured{Object: map[string]any{}}} + want := int64(123) + u.SetObservedGeneration(want) + + if got := u.GetObservedGeneration(); *got != want { + t.Errorf("u.GetObservedGeneration() got: %v, want %v", *got, want) + } + if g := u.GetUnstructured().Object["status"].(map[string]any)["observedGeneration"]; g != want { + t.Errorf("Generations do not match! got: %v (%T)", g, g) + } +} + +func TestObservedGenerationNotFound(t *testing.T) { + u := &Unstructured{unstructured.Unstructured{Object: map[string]any{}}} + + if g := u.GetObservedGeneration(); g != nil { + t.Errorf("u.GetObservedGeneration(): expected nil, but got %v", g) + } +} + +func TestObservedLabels(t *testing.T) { + u := &Unstructured{unstructured.Unstructured{Object: map[string]any{}}} + labels := map[string]string{ + "foo": "1", + "bar": "2", + } + u.SetObservedLabels(labels) + + if diff := cmp.Diff(labels, u.GetObservedLabels()); diff != "" { + t.Errorf("u.GetObservedLabels(): -want, +got:\n%s", diff) + } +} + +func TestObservedLabelsNotFound(t *testing.T) { + u := &Unstructured{unstructured.Unstructured{Object: map[string]any{}}} + + if g := u.GetObservedLabels(); g != nil { + t.Errorf("u.GetObservedLabels(): expected nil, but got %v", g) + } +} + +func TestObservedAnnotations(t *testing.T) { + u := &Unstructured{unstructured.Unstructured{Object: map[string]any{}}} + annotations := map[string]string{ + "foo": "1", + "bar": "2", + } + u.SetObservedAnnotations(annotations) + + if diff := cmp.Diff(annotations, u.GetObservedAnnotations()); diff != "" { + t.Errorf("u.GetObservedAnnotations(): -want, +got:\n%s", diff) + } +} + +func TestObservedAnnotationsNotFound(t *testing.T) { + u := &Unstructured{unstructured.Unstructured{Object: map[string]any{}}} + + if g := u.GetObservedAnnotations(); g != nil { + t.Errorf("u.GetObservedAnnotations(): expected nil, but got %v", g) + } +}