-
Notifications
You must be signed in to change notification settings - Fork 101
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
pkg/resource: consistent applicators #491
base: main
Are you sure you want to change the base?
Conversation
pkg/resource/resource.go
Outdated
// An PartialApplicator applies changes to an object. The passed object can be | ||
// partial, preserving parts of the object that are managed by other actors. | ||
type PartialApplicator interface { | ||
ApplyPartialObject(context.Context, client.Object, ...ApplyOption) error |
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.
from discussion with @pedjak: an interface like that won't work for SSA as client.Object
cannot be partial, maybe with exception of an *unstructured.Unstructured
. Sending full objects to SSA won't end well.
d3a9ba0
to
520a765
Compare
520a765
to
97f3757
Compare
764512b
to
72874ef
Compare
This needs a proof PR against Crossplane. I have the suspicion that it breaks the composite reconciler as it relies on the inconsistent, additive behaviour. |
Perhaps we can add a few e2e tests to validate that? (in crossplane PR ofcourse) |
Agreed. I haven't had time to review closely. I like the direction but suspect it could be breaking in a few places. |
pkg/resource/api.go
Outdated
func (a *APIPatchingApplicator) WithLogger(l logging.Logger) *APIPatchingApplicator { | ||
a.optionalLog = l | ||
return a | ||
} |
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.
This is introducing a second way to pass options (builder style) in addition to the variadic ApplyOption
style supported by the Apply
method.
Given that:
- As you mention
LogDiff
is expensive, so we want it to be done optionally. LogDiff
is called right after where we loop over the variadic option functions.LogDiff
has a very similar signature to the variadic option functions.
Could LogDiff
itself be an option - e.g. p.Apply(ctx, o, WithLogDiff(l logging.Logger))
?
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 had that in the beginning and forgot why I reverted it.
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 think the main reason is that I couldn't find a case where I want per-call log settings.
pkg/resource/api.go
Outdated
// Note: this check would ideally not be necessary if the Apply signature | ||
// had a current object that we could us for the diff. But we have no | ||
// current and for consistency of the patch it matters that the object we | ||
// get above is the one that was originally used. |
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.
Could we ab(use) the apply options to optionally provide the current
object? e.g. p.Apply(ctx, desired, WithCurrent(observed))
.
Presumably even if there was we'd still need to fall back to
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.
possibly. Note though that our clients are cached. So it doesn't make a big difference.
@sttts +1 for deprecating the With regard to the Is the fact that the |
It has a consistency issue though :) It does not apply what you tell it to apply. It additively merges your changes into the version on the server.
Yes, it is. But this of course means that it wipes everything not in the passed object. E.g. if you override the spec in the composition controller code to be equal to that in the |
pkg/resource/api.go
Outdated
return errors.Wrap(a.client.Create(ctx, o), "cannot create object") | ||
func (a *APIPatchingApplicator) Apply(ctx context.Context, obj client.Object, ao ...ApplyOption) error { //nolint:gocyclo // the logic here is crucial and deserves to stay in one method | ||
if obj.GetName() == "" && obj.GetGenerateName() != "" { | ||
return errors.Wrap(a.client.Create(ctx, obj), "cannot create object") |
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.
in case of error, would be nice to give some details about obj
that could not be created?
return errors.Wrapf(a.client.Create(ctx, obj), "cannot create object %s of %s", obj.GetGenerateName(), obj.GetObjectKind().GroupVersionKind())
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.
Had added that context and removed it again because those error messages already contain the GR and name.
pkg/resource/resource.go
Outdated
@@ -262,6 +273,30 @@ func UpdateFn(fn func(current, desired runtime.Object)) ApplyOption { | |||
} | |||
} | |||
|
|||
// LogDiff logs the diff between current and desired object. | |||
func LogDiff(log logging.Logger, current, desired runtime.Object) error { |
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.
since log
can be nil
, perhaps we could rename method toMaybeLogDiff
to improve readability? Also, does it need to be public?
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.
fixed
pkg/logging/logging.go
Outdated
@@ -92,3 +93,20 @@ func (l logrLogger) Debug(msg string, keysAndValues ...any) { | |||
func (l logrLogger) WithValues(keysAndValues ...any) Logger { | |||
return logrLogger{log: l.log.WithValues(keysAndValues...)} //nolint:logrlint // False positive - logrlint thinks there's an odd number of args. | |||
} | |||
|
|||
// ForResource returns logging values for a resource. | |||
func ForResource(object client.Object) []string { |
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.
it seems that this is not used anywhere in the code?
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.
but should be, see you comment above about the error context.
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.
added
pkg/resource/api.go
Outdated
return err | ||
} | ||
} | ||
|
||
if err := LogDiff(a.optionalLog, current, obj); err != nil { |
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.
given that diff compute is expensive, could we avoid doing it twice, in case when optionalLog
is set? It is computed by client.MergeFromWithOptions(current, client.MergeFromWithOptimisticLock{}))
anyway, so perhaps we can create a patch wrapper around it to cache the computed value?
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.
good idea. done
f6b7ab7
to
f9356f8
Compare
Addressed comments. Also extended tests. |
pkg/resource/api.go
Outdated
func (a *APIUpdatingApplicator) Apply(ctx context.Context, obj client.Object, ao ...ApplyOption) error { | ||
if obj.GetName() == "" && obj.GetGenerateName() != "" { | ||
log := a.log.WithValues(logging.ForResource(obj)) | ||
log.Info("creating object") |
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.
perhaps debug level?
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.
No, this is intentional. This is progress output, not debug.
Note: there is no output if nothing happens, i.e. in the steady state.
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.
So, would this mean we would have one log line per created object during their creation? Which I believe would still be too verbose for default logging and not inline with the existing logging behavior in XP.
Also, there is another client.Create
line below, why don't we have a similar log line below? Are they different cases?
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.
Also, there is another client.Create line below, why don't we have a similar log line below? Are they different cases?
fixed.
So, would this mean we would have one log line per created object during their creation?
yes
Which I believe would still be too verbose for default logging and not inline with the existing logging behavior in XP
It's intentionally not inline with the existing logging behaviour. The current behaviour is not useful.
Counter question: why is it ok to log the same in the apiserver, but not where it matters for understanding what a component does?
How many creates or updates do we expect in a reasonably big XP installation on average? My claim: the system is in steady state 99% of the time without any update to the apiserver.
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.
It's intentionally not inline with the existing logging behaviour. The current behaviour is not useful.
I believe this deserves some upfront discussion and alignment as it contradicts the current observability guide, for example:
Almost nothing is worth logging at info level. When deciding which logging level to use, consider a production deployment of Crossplane reconciling tens or hundreds of managed resources. If in doubt, pick debug. You can easily increase the log level later if it proves warranted.
And I personally would agree with the current approach we're following as I believe events and conditions are better fit for emitting "per instance" information.
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.
Have moved the logging parts into #532. Please continue discussion there. And yes, we need a bigger discussion around what to log.
I believe events and conditions are better fit for emitting "per instance" information.
I disagree. Neither events nor conditions are for CRUD or reconcile logging. Logs are lower level, component level, not API level.
log := a.log.WithValues(logging.ForResource(obj)) | ||
log.WithValues("diff", string(patchBytes)).Info("updating object") | ||
|
||
return a.client.Update(ctx, obj) |
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.
just wonder in case of error, would be able to see something in logs and correlate it with the previous log line? Also, do we really want to emit log line on INFO?
Should we log here in case of an error?
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.
the clients produce errors with context. No need to add another one.
Error processing and error logging I leave to the caller. controller-runtime will also log if the error is passed up. If the error is expected, the caller will do the thing and not log. We don't know here.
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.
But, I would like to see this in action in a proof PR in Crossplane.
pkg/resource/api.go
Outdated
// This only works with a desired object of type *unstructured.Unstructured. | ||
// | ||
// Deprecated: replace with Server Side Apply. | ||
func AdditiveMergePatchApplyOption(ctx context.Context, current, desired runtime.Object) error { |
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.
7301d90
to
bf9437f
Compare
The logic in the composite controller (also in the claim controller) heavily relies on additive behavior currently. Hence, it wouldn't be possible to switch to the patching applicator introduced in this PR without a good amount of refactoring/testing, which could be a similar amount of effort as switching to Server Side Apply. IIUC, this will work quite similar to the extract flow mentioned here. |
} | ||
} | ||
|
||
// NOTE(hasheddan): we must set the resource version of the desired object | ||
// to that of the current or the update will always fail. | ||
m.SetResourceVersion(current.(metav1.Object).GetResourceVersion()) |
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.
Now update call fails without a resource version:
metadata.resourceVersion: Invalid value: 0x0: must be specified for an update
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.
the object passed to the applicator here is based on a Get
, i.e. has a resource version. Any other use of the applicator is invalid.
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.
We pass the current
(which is a deepcopy of obj) to the Get
. So, the object passed to Update does not have a resource version.
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.
Ah ok, you mean "should be based on a Get
" ? Confused because, currently this is not the case for most of the usages of Apply.
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.
Note that AdditiveMergePatchApplyOption
additively merges. This means that the RV from current
is copied into desired
. Hence, we always have a RV if Get
has been successful. And if it hasn't, we create object. So we should be fine.
pkg/resource/api.go
Outdated
// | ||
// Deprecated: replace with Server Side Apply. | ||
func AdditiveMergePatchApplyOption(_ context.Context, current, desired runtime.Object) error { | ||
u, ok := desired.(*unstructured.Unstructured) |
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.
desired object is usually our wrapper types, e.g. composed.Unstructured or composite.Unstructured, so would fail here.
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.
ErrTooManyWrappers. Fixing.
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.
done
Signed-off-by: Dr. Stefan Schimanski <stefan.schimanski@upbound.io>
f7474f1
to
1b63ad4
Compare
Hence, |
1b63ad4
to
79f1338
Compare
Signed-off-by: Dr. Stefan Schimanski <stefan.schimanski@upbound.io>
…e applicator Signed-off-by: Dr. Stefan Schimanski <stefan.schimanski@upbound.io>
79f1338
to
6747cf0
Compare
type Applicator interface { | ||
// Apply updates the given object to exactly the given state. It conflicts | ||
// if the resource version stored on the apiserver does not match anymore | ||
// the one in the given object. |
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.
@turkenh this comment and the one above are crucial. Any other behaviour must go into an ApplyOption
.
Independently from the strategy, we need to create crossplane e2e tests, covering at least major usecases advocated to users - only then we can switch with confidence to an improved applicator/server-side apply. |
0aa37e8
to
2bf1830
Compare
Signed-off-by: Dr. Stefan Schimanski <stefan.schimanski@upbound.io>
2bf1830
to
9a0fe90
Compare
Hey @sttts , is this PR still relevant? |
The applicators are semantically as broken as before. So yes, it's relevant. |
Description of your changes
This PR does two things:
Fixes #483:
make reviewable test
to ensure this PR is ready for review.How has this code been tested