-
Notifications
You must be signed in to change notification settings - Fork 132
Better support for non-deterministic external-names by updating critical annotations #850
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
base: main
Are you sure you want to change the base?
Changes from all commits
4fa7610
845246c
a5c0e9e
c14e6da
fc6a3f5
5c7389d
11e2136
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -55,6 +55,15 @@ const ( | |
| errUpdateCriticalAnnotations = "cannot update critical annotations" | ||
| ) | ||
|
|
||
| var ( | ||
| criticalAnnotations = []string{ | ||
| meta.AnnotationKeyExternalCreateFailed, | ||
| meta.AnnotationKeyExternalCreatePending, | ||
| meta.AnnotationKeyExternalCreateSucceeded, | ||
| meta.AnnotationKeyExternalName, | ||
| } | ||
| ) | ||
|
|
||
| // NameAsExternalName writes the name of the managed resource to | ||
| // the external name annotation field in order to be used as name of | ||
| // the external resource in provider. | ||
|
|
@@ -277,15 +286,36 @@ func NewRetryingCriticalAnnotationUpdater(c client.Client) *RetryingCriticalAnno | |
| // UpdateCriticalAnnotations updates (i.e. persists) the annotations of the | ||
| // supplied Object. It retries in the face of any API server error several times | ||
| // in order to ensure annotations that contain critical state are persisted. | ||
| // Pending changes to the supplied Object's spec, status, or other metadata | ||
| // might get reset to their current state according to the API server, e.g. in | ||
| // case of a conflict error. | ||
| // Only annotations will be updated as part of this operation, other fields of the | ||
| // supplied Object will not be modified. | ||
| func (u *RetryingCriticalAnnotationUpdater) UpdateCriticalAnnotations(ctx context.Context, o client.Object) error { | ||
| a := o.GetAnnotations() | ||
| a := make(map[string]string) | ||
| for _, k := range criticalAnnotations { | ||
| if v, ok := o.GetAnnotations()[k]; ok { | ||
| a[k] = v | ||
| } | ||
| } | ||
|
|
||
| if len(a) == 0 { | ||
| // No critical annotations to update. | ||
| return nil | ||
| } | ||
|
|
||
| err := retry.OnError(retry.DefaultRetry, func(err error) bool { | ||
| return !errors.Is(err, context.Canceled) | ||
| }, func() error { | ||
| err := u.client.Update(ctx, o) | ||
| patchMap := map[string]interface{}{ | ||
| "metadata": map[string]any{ | ||
| "annotations": a, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we be more selective in what's being patched here, i.e., not all the annotations on the MR are the critical ones and we would only like to manage the critical annotations by this manager? This will probably not be an issue as this manager will not have an opinion on "non-critical" annotations and their respective managers will dictate their values. But one potential issue is when the other manager (who should really be owning the annotation) actually wants to delete the annotation. The
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be a reasonable addition for this function to have knowledge about what is a "critical annotation". Currently that comes from the execution order of the reconciler, this way we would make it explicit.
Am I missing any?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've added a very naive approach in 11e2136 |
||
| }, | ||
| } | ||
|
|
||
| patchData, err := json.Marshal(patchMap) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| err = u.client.Patch(ctx, o, client.RawPatch(types.MergePatchType, patchData), client.FieldOwner(fieldOwnerAPISimpleRefResolver), client.ForceOwnership) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like we patch all annotations here.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We had better use a different manager name than
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it make sense for the MR reconciler to just use one manager name, regardless of operation? Possibly too late if we're already using |
||
| if kerrors.IsConflict(err) { | ||
| if getErr := u.client.Get(ctx, client.ObjectKeyFromObject(o), o); getErr != nil { | ||
| return getErr | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1408,6 +1408,21 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (resu | |
| return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedStatus) | ||
| } | ||
|
|
||
| if observation.ResourceExists { | ||
| // When a resource exists or is just created, it might have received | ||
| // a non-deterministic external name after its creation, which we need to persist. | ||
| // We do this by updating the critical annotations. | ||
| // This is needed because some resources might not receive an external-name directly | ||
| // after the creation, but later as part of an asynchronous process. | ||
| // When Crossplane supports asynchronous creation of resources natively, this logic | ||
| // might not be needed anymore and can be revisited. | ||
| if err := r.managed.UpdateCriticalAnnotations(ctx, managed); err != nil { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. a thing to consider (possible nit): Especially in async creations with long-running creation times, currently (with no native async support) the external clients return
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps we should make writing these annotations optional. I'd suggest making them opt-out since that's the safest path. The option would be specified by the provider author - so they can disable these annotations if they know for sure they're not needed (i.e. naming is deterministic, API is strongly consistent). I remember discussing this with someone recently, but can't find a tracking issue. (If we do make them optional, I think we could do it pretty easily by injecting a no-op implementation of the annotation updater.)
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It should also be straightforward to keep a state in the reconciler whether a critical annotation was added/changed and depending on that update the annotations or perform a no-op. On the other hand I'm wondering whether this kind of optimization is needed for "Critical" annotations. Shouldn't the priority be here to add those annotations? Otherwise it would leave room for errors. |
||
| log.Debug(errUpdateManagedAnnotations, "error", err) | ||
| record.Event(managed, event.Warning(reasonCannotUpdateManaged, errors.Wrap(err, errUpdateManagedAnnotations))) | ||
| return reconcile.Result{Requeue: true}, errors.Wrap(r.client.Status().Update(ctx, managed), errUpdateManagedAnnotations) | ||
| } | ||
| } | ||
|
Comment on lines
+1411
to
+1424
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As @lsviben explains here, upjet should not be relying on setting |
||
|
|
||
| if observation.ResourceLateInitialized && policy.ShouldLateInitialize() { | ||
| // Note that this update may reset any pending updates to the status of | ||
| // the managed resource from when it was observed above. This is because | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this update operation is being replaced by SSA.