Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to expose resource reconciliation progress #633

Merged
merged 2 commits into from
Apr 19, 2024

Conversation

pedjak
Copy link
Contributor

@pedjak pedjak commented Dec 27, 2023

Description of your changes

  • Optional 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.
  • Optional status.conditions[x].observedGeneration represents 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:

  • Read and followed Crossplane's contribution process.
  • Run make reviewable test to ensure this PR is ready for review.

@pedjak pedjak requested review from a team as code owners December 27, 2023 16:09
@pedjak pedjak requested review from negz and MisterMX December 27, 2023 16:09
Comment on lines 116 to 124
// 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"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

Copy link
Member

@negz negz Jan 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.generation did not change, status.observedGeneration does 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?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we stop propagating annotations/labels from claim to composite?

I don't think we can - this would be a breaking behaviour change.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you expect that managed resources would also satisfy this interface, eventually?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@sttts
Copy link
Contributor

sttts commented Mar 17, 2024

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?

@negz
Copy link
Member

negz commented Mar 25, 2024

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:

  • There's a school of thought that we shouldn't propagate them at all.
  • observedGeneration is an established pattern in the Kobe world, but I don't think observedLabels etc is.
  • observedGeneration copies only the generation int, labels etc we'd need to copy the whole object.

@@ -226,6 +226,7 @@ type ResourceSpec struct {
// 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.

* `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 <[email protected]>
@pedjak pedjak force-pushed the add-reconcile-observation branch from 8fd92c1 to b5462b5 Compare April 9, 2024 09:10
@pedjak
Copy link
Contributor Author

pedjak commented Apr 9, 2024

Yes, I think we should do that.

@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.

@sttts
Copy link
Contributor

sttts commented Apr 18, 2024

@negz can you take another look?

Comment on lines 372 to 391
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)
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@@ -223,7 +223,7 @@ func (c *Unstructured) SetPublishConnectionDetailsTo(ref *xpv1.PublishConnection

// GetCondition of this composite resource claim.
func (c *Unstructured) GetCondition(ct xpv1.ConditionType) xpv1.Condition {
conditioned := xpv1.ConditionedStatus{}
conditioned := xpv1.ResourceStatus{}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why change this to ResourceStatus?

@@ -233,7 +233,7 @@ func (c *Unstructured) GetCondition(ct xpv1.ConditionType) xpv1.Condition {

// SetConditions of this composite resource claim.
func (c *Unstructured) SetConditions(conditions ...xpv1.Condition) {
conditioned := xpv1.ConditionedStatus{}
conditioned := xpv1.ResourceStatus{}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why change this to ResourceStatus?

@@ -72,7 +72,7 @@ func (cr *Unstructured) GetUnstructured() *unstructured.Unstructured {

// GetCondition of this Composed resource.
func (cr *Unstructured) GetCondition(ct xpv1.ConditionType) xpv1.Condition {
conditioned := xpv1.ConditionedStatus{}
conditioned := xpv1.ResourceStatus{}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why change this to ResourceStatus?

@@ -82,7 +82,7 @@ func (cr *Unstructured) GetCondition(ct xpv1.ConditionType) xpv1.Condition {

// SetConditions of this Composed resource.
func (cr *Unstructured) SetConditions(c ...xpv1.Condition) {
conditioned := xpv1.ConditionedStatus{}
conditioned := xpv1.ResourceStatus{}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why change this to ResourceStatus?


// SetObservedGeneration of this composite resource claim.
func (c *Unstructured) SetObservedGeneration(generation int64) {
status := &xpv1.ResourceStatus{}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use ObservedStatus? (Here and elsewhere)

Signed-off-by: Predrag Knezevic <[email protected]>
@pedjak
Copy link
Contributor Author

pedjak commented Apr 19, 2024

I have addressed your comments @negz ptal.

Copy link
Member

@negz negz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

@negz negz merged commit b121916 into crossplane:master Apr 19, 2024
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Enable user to understand when external resource is reconciled
3 participants