-
Notifications
You must be signed in to change notification settings - Fork 143
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
8dfcfd1
b57908c
64183fc
d1ce6f1
a3516db
fd69032
eb17cb3
5035730
7e15f7f
5fbe261
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,16 @@ const ( | |
| errUpdateCriticalAnnotations = "cannot update critical annotations" | ||
| ) | ||
|
|
||
| //nolint:gochecknoglobals // this is a list of critical annotations that should be retried when updating, and is not expected to be modified at runtime. | ||
| 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 +287,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]any{ | ||
| "metadata": map[string]any{ | ||
| "annotations": a, | ||
|
twobiers marked this conversation as resolved.
|
||
| }, | ||
| } | ||
|
|
||
| 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) | ||
|
twobiers marked this conversation as resolved.
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 |
|---|---|---|
|
|
@@ -1410,6 +1410,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
+1413
to
+1426
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.