Add ability to expose resource reconciliation progress#633
Add ability to expose resource reconciliation progress#633negz merged 2 commits intocrossplane:masterfrom
Conversation
apis/common/v1/condition.go
Outdated
| // 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"` |
There was a problem hiding this comment.
| // 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"` | |
| // ObservedLabels are the most recent resource metadata.labels | |
| // observed by the reconciler | |
| // +optional | |
| ObservedLabels map[string]string `json:"observedLabels,omitempty"` | |
| // ObservedLabels are the most recent resource metadata.annotations | |
| // observed by the reconciler | |
| // +optional | |
| ObservedAnnotations map[string]string `json:"observedAnnotations,omitempty"` |
Can you please create a new struct (something like ObservedStatus) for these fields, instead of adding them to ConditionedStatus. The idea of ConditionedStatus is that you can embed it in a status struct to support status conditions. The new struct should be in a different file (not condition.go).
Is there precedent for other tools having observedLabels and observedAnnotations as well as observedGeneration? I'm supportive of observedGeneration but not convinced that it's worth two additional fields for labels and annotations.
There was a problem hiding this comment.
@negz great that you spot the typo!
The new struct should be in a different file (not condition.go).
Done, the struct lives now in apis/common/v1/observation.go
Is there precedent for other tools having observedLabels and observedAnnotations as well as observedGeneration? I'm supportive of observedGeneration but not convinced that it's worth two additional fields for labels and annotations.
Value in metadata.generation get increased only when resource spec changes. Given that our controllers react also on label/annotation changes and often use/propagate them during the reconciliation, writing down just the observed generation might not be enough, i.e. an observer would not be able to detect if/when a label/annotation change got processed.
There was a problem hiding this comment.
I'm sold on observedGeneration because:
- It's succinct, just a generation number not a copy of the whole spec object.
- It's an established Kubernetes pattern - it's common (and recommended iirc) to use
observedGeneration. - Reacting to spec (i.e. desired state) changes is pretty much always critical.
However I'm not sold on observedLabels and observedAnnotations. Compared to observedGeneration:
- It's more verbose - we have to duplicate all labels and annotations under status.
- It's not a pattern I've ever seen before. (I didn't search though - let me know if I'm mistaken.)
- Reacting to label/annotation changes seems less clearly critical. I can see that it could be in some cases, but it doesn't seem as clear cut at spec.
Do we know of concrete situations (ideally issues) that recording observed labels and annotations would have helped with? If not, I would suggest we omit them until there's evidence they're needed.
There was a problem hiding this comment.
Do we know of concrete situations (ideally issues) that recording observed labels and annotations would have helped with? If not, I would suggest we omit them until there's evidence they're needed.
The PR adds an ability to record the observed labels/annotations and the both fields are optional. IMHO a controller should set them if they are used anyhow to influence the reconcilation logic.
In a Crossplane claim, its annotations and labels are copied down to its counterpart composite as a part of reconciliation.
If we do not record the observed claim annotations/lables, then an observer is not able to detect if a reconciliation has happened:
- A claim got created and became ready
- The claim annotation got added/removed/updated
- Given that
metadata.generationdid not change,status.observedGenerationdoes not change either after the reconcile
I have found an old issue about asking to increase metadata.generation on annotation/label changes:
kubernetes/kubernetes#67428 and very interesting followup discussion.
@sttts what are your thought on this? Should we stop propagating annotations/labels from claim to composite? Should it be acceptable if claim reconciliation on pure annotation/label changes is not detectable by an observer?
There was a problem hiding this comment.
If we do not record the observed claim annotations/lables, then an observer is not able to detect if a reconciliation has happened.
I get that, but I'm challenging whether this has ever been a real problem for someone. Should we wait to see if this is hurting people before we fix it?
There was a problem hiding this comment.
Should we stop propagating annotations/labels from claim to composite?
I don't think we can - this would be a breaking behaviour change.
There was a problem hiding this comment.
The PR adds an ability to record the observed labels/annotations and the both fields are optional. IMHO a controller should set them if they are used anyhow to influence the reconcilation logic.
In a Crossplane claim, its annotations and labels are copied down to its counterpart composite as a part of reconciliation.
If we do not record the observed claim annotations/lables, then an observer is not able to detect if a reconciliation has happened
I tend to agree with these statements.
That having said, IMO passing down labels/annotations from claim to xrd and similar is wrong, especially for annotations. But as they are passed down today, it sounds logical that the controller records them as part of the observed state.
|
|
||
| SetObservedAnnotations(annotations map[string]string) | ||
| GetObservedAnnotations() map[string]string | ||
| } |
There was a problem hiding this comment.
Do you expect that managed resources would also satisfy this interface, eventually?
There was a problem hiding this comment.
Do you expect that managed resources would also satisfy this interface, eventually?
Yes, I was just thinking that adding ReconciliationObserver to Managed would be too much for this PR, because then we would need to add the necessary logic to pkg/reconciler/managed/reconciler.go.
8a33531 to
8fd92c1
Compare
|
Which labels and annotations do we have in mind here? In the Kubernetes object model neither should really matter, the spec does. How can we move this forward? If the observedGeneration is a no-brainer, can we move that forward first? |
Yes, I think we should do that. The labels/annotations seems trickier, so I want to hold on that for now:
|
| // ResourceStatus represents the observed state of a managed resource. | ||
| type ResourceStatus struct { | ||
| ConditionedStatus `json:",inline"` | ||
| ObservedStatus `json:",inline"` |
This comment was marked as duplicate.
This comment was marked as duplicate.
Sorry, something went wrong.
This comment was marked as duplicate.
This comment was marked as duplicate.
Sorry, something went wrong.
* `status.observedGeneration` fields has been added to claim/composite/composed status, showing the latest metadata.generation which resulted in either a ready state, or stalled due to error it can not recover from without human intervention. * `status.conditions[x].observedGeneration represents the .metadata.generation that the condition was set based upon Signed-off-by: Predrag Knezevic <predrag.knezevic@upbound.io>
8fd92c1 to
b5462b5
Compare
@negz @sttts I have updated PR, aligned also with SIG Architecture API convention. This opens up the possibility to achieve compatibility with kstatus specification at some point. |
|
@negz can you take another look? |
| 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 != 0 { | ||
| t.Errorf("u.GetObservedGeneration(): expected nil, but got %v", g) | ||
| } | ||
| } |
There was a problem hiding this comment.
These tests are written in a different style from every other test in the file. Despite the fact that they're mostly tables with a single case, I'd prefer to match the existing practices.
| @@ -141,6 +157,9 @@ func (s *ConditionedStatus) SetConditions(c ...Condition) { | |||
|
|
|||
| if existing.Equal(new) { | |||
There was a problem hiding this comment.
GitHub won't let me add a comment to the existing.Equal docstring, but it should explicitly call out that it doesn't consider ObservedGeneration when assessing equality. It already calls out that it ignores LastTransitionTime.
| // GetCondition of this composite resource claim. | ||
| func (c *Unstructured) GetCondition(ct xpv1.ConditionType) xpv1.Condition { | ||
| conditioned := xpv1.ConditionedStatus{} | ||
| conditioned := xpv1.ResourceStatus{} |
There was a problem hiding this comment.
Why change this to ResourceStatus?
| // SetConditions of this composite resource claim. | ||
| func (c *Unstructured) SetConditions(conditions ...xpv1.Condition) { | ||
| conditioned := xpv1.ConditionedStatus{} | ||
| conditioned := xpv1.ResourceStatus{} |
There was a problem hiding this comment.
Why change this to ResourceStatus?
| // GetCondition of this Composed resource. | ||
| func (cr *Unstructured) GetCondition(ct xpv1.ConditionType) xpv1.Condition { | ||
| conditioned := xpv1.ConditionedStatus{} | ||
| conditioned := xpv1.ResourceStatus{} |
There was a problem hiding this comment.
Why change this to ResourceStatus?
| // SetConditions of this Composed resource. | ||
| func (cr *Unstructured) SetConditions(c ...xpv1.Condition) { | ||
| conditioned := xpv1.ConditionedStatus{} | ||
| conditioned := xpv1.ResourceStatus{} |
There was a problem hiding this comment.
Why change this to ResourceStatus?
|
|
||
| // SetObservedGeneration of this composite resource claim. | ||
| func (c *Unstructured) SetObservedGeneration(generation int64) { | ||
| status := &xpv1.ResourceStatus{} |
There was a problem hiding this comment.
Why not use ObservedStatus? (Here and elsewhere)
Signed-off-by: Predrag Knezevic <predrag.knezevic@upbound.io>
|
I have addressed your comments @negz ptal. |
Description of your changes
status.observedGenerationfields has been added to claim/composite/composed status, showing the latest metadata.generation which resulted in either a ready state, or stalled due to error it can not recover from without human intervention.status.conditions[x].observedGenerationrepresents the .metadata.generation that the condition was set based upon.Conditions are now aligned with SIG Architecture API convention.
Fixes crossplane/crossplane#4695
Part of crossplane/crossplane#4655
I have:
make reviewable testto ensure this PR is ready for review.