-
Notifications
You must be signed in to change notification settings - Fork 98
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
Support merging in addition to replacing objects in patches #270
Conversation
pkg/fieldpath/merge.go
Outdated
KeepMapValues bool `json:"keepMapValues,omitempty"` | ||
// Specifies that already existing elements in a merged slice should be preserved | ||
AppendSlice bool `json:"appendSlice,omitempty"` | ||
} |
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.
Is the intent that this type would be embedded in API types, as in https://github.com/crossplane/crossplane/pull/2409/files#diff-40b5c705a7ba08a514b14b156d26120fcd0c6a8b4326096ceecbb7d3bd971f29R283?
If so, I would prefer it to be defined along with all other API types in https://github.com/crossplane/crossplane-runtime/tree/master/apis/common/v1. I'd also suggest we make sure it complies with API conventions. Specifically:
- Are we sure boolean fields are the best option here? API conventions warns to think twice about them.
- Are these fields optional? If so they should be marked
// +optional
and be*bool
.
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.
Yes, it's embedded in API types as you mentioned. Moved it to the suggested package.
Regarding API conventions, we could go with a string type alias MergePolicy
and define policy constants with it but we may add other options that control merge behavior and we will (assuming they are not mutually exclusive) need cartesian product of these options. So I preferred a bitmap-like structure.
Added the +optional
marker and converted the types from bool
to *bool
. Asking to learn: FWIW, here for these two fields, we have the same semantics for the zero-values of both bool
and *bool
. Do we have another reason for the conversion apart from being probably a better practice?
pkg/fieldpath/merge.go
Outdated
return nil, false, err | ||
} | ||
return Pave(oMap), true, 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.
Is merge.go
the right place for this function? It does not seem specific to merging.
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.
Moved it to another package (fieldpath/object
). Please see below for a discussion.
pkg/resource/api.go
Outdated
@@ -152,6 +153,15 @@ func (a *APIPatchingApplicator) Apply(ctx context.Context, o client.Object, ao . | |||
return errors.Wrap(a.client.Patch(ctx, o, &patch{desired}), "cannot patch object") | |||
} | |||
|
|||
// WithMergeOptions returns an ApplyOption for merging the value at the given | |||
// fieldPath of desired object onto the current object with | |||
// the given merge options. |
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 wonder whether we should consider moving to server side apply rather than building our own merge implementation client side?
https://kubernetes.io/docs/reference/using-api/server-side-apply/#merge-strategy
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 will take a look.
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.
Hi @negz,
Took a look at server-side apply. My understanding is that it is mainly addressing a somewhat different kind of a problem than the feature we are trying to implement, though we can employ conflict resolution mechanics of server-side apply to achieve merging/replacing fields as you suggested. And this does not mean that we cannot change the feature formulation (or that my understanding does not have flaws).
My understanding is that server-side apply primarily aims a "more stable object lifecycle" by keeping track of appliers (aka fieldManagers) as the managers of a set of fields, and giving a conflict if a field is being modified by another applier than the current fieldManager. We can control how field ownership will be defined, at "compile" time, via merge strategies, e.g., define a merge-strategy of granular
for a map if we would like to allow different appliers to control different keys of a map, or a merge-strategy of atomic
if would like the map to be managed by a single applier with all its keys.
With the current implementation here, we are allowing a composition to specify the merge strategy at runtime and it works bidirectionally, i.e., while merging from composite into composed or from composed into composite. One composition can request a different merge strategy than another.
I'm not sure but server-side apply sounded to me like something of a broader context than adding this feature. For example, I think we may want to reconsider resource.APIPatchingApplicator
if we decide to employ apply patches and the reconcilers themselves (though we may also choose to use server-side apply only in specific contexts).
pkg/fieldpath/merge.go
Outdated
|
||
// MergePath merges the value at the given field path of the src object into | ||
// the dst object. | ||
func MergePath(fieldPath string, dst, src runtime.Object, mergeOptions *MergeOptions) 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.
I'm not sure how I feel about making this package deal more directly with runtime.Object
- i.e. taking them as an argument. So far the established pattern has been to convert a runtime.Object
(or anything else) to a *Paved
, then operate on that. What do you think about implementing MergePath
as a method on *Paved
similar to the current SetValue
? For example:
func (p *Paved) MergeValue(path string, value interface{}, o ...MergeOption) 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.
Added Paved.MergeValue
as suggested.
I see benefit in introducing a layer which can handle runtime.Object
s so that as demonstrated in the dependent PR crossplane/crossplane/pull/2409, so I moved those newly introduced functions handling with runtime.Object
into the fieldpath/object
package. We could also move them to core crossplane. What do you think?
Signed-off-by: Alper Rifat Ulucinar <ulucinar@users.noreply.github.com>
- Add MergeReplace in fieldpath package for ease of writing tests. Signed-off-by: Alper Rifat Ulucinar <ulucinar@users.noreply.github.com>
- Add "fieldpath/object" package that deals with runtime.Objects - Move MergeOptions struct to package "apis/common/v1". Signed-off-by: Alper Rifat Ulucinar <ulucinar@users.noreply.github.com>
pkg/fieldpath/object/object.go
Outdated
// ToPaved tries to convert a runtime.Object into a *Paved via | ||
// runtime.DefaultUnstructuredConverter if needed. Returns the paved if | ||
// the conversion is successful along with whether the | ||
// runtime.DefaultUnstructuredConverter has been employed during the | ||
// conversion. |
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 see that when you call this function you name the bool
return value copied
. It would be useful to include this information in the docstring. From my reader's perspective it is not obvious what the implications of runtime.DefaultUnstructuredConverter
being employed is - I can only infer from the function below that it means the returned value is a copy when it would otherwise not be.
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, note that there is already a fieldpath.PaveObject
function that has very similar functionality to this one. The key difference being that it always invokes the DefaultUnstructuredConverter
. Do we need both?
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.
Yes, I also do not like that we have two functions with exactly the same duty. The newly added one (ToPaved
), as you see, has a shortcut for types that provide an UnstructuredContent()
method, a performance optimization that we can live without. We can remove ToPaved
and always copy back to the runtime.Object
at the call site via runtime.DefaultUnstructuredConverter.FromUnstructured
.
Because fieldpath.PaveObject
is already part of the public API, I did not want to remove it. Maybe we can declare it as deprecated in favor of ToPaved
. I don't have a strong opinion 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.
If we decide to keep ToPaved
, I will update the documentation as you suggested above.
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.
Thanks! I don't have a strong opinion either. Usually I would bias for API simplicity over performance optimisations though, at least until we experience noticeable performance issues. In this case I'm happy for you to choose - we can either deprecate the old function or not add the new one.
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.
Removed fieldpath.ToPaved
and kept fieldpath.PaveObject
.
pkg/fieldpath/object/object.go
Outdated
return fieldpath.Pave(oMap), true, nil | ||
} | ||
|
||
// PatchFieldValueToObject applies the value to the "to" object at the given |
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'm still a little uncomfortable with the fact that we're adding what appear to be convenience functions that operate on runtime.Object
for patching, but not for any other part of the fieldpath
package.
My concern is not in thinking that runtime.Object
is the wrong level to operate at, but rather that merging now appears to be a special case that is asymmetrical from the rest of the API. If the caller wants to set or get a value they handle conversion to a *Paved
and use SetValue
and GetValue
. However if the caller wants to patch, they use this special package that operates on runtime.Object
.
Perhaps this makes sense if you imagine this package would be expanded to handle more than just merging with time?
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 agree. That's why I had also suggested that we can maybe move these convenience functions to core crossplane. The Paved.MergeValue
is coherent with the rest of the fieldpath
package, but we also need some higher level functions that will extract the value to be merged into the destination from a source runtime.Object
, as you also acknowledge.
What do you think about moving these convenience functions to crossplane/crossplane
(I'm calling this as "core crossplane", not sure if it conveys the right meaning)? I'm also fine if they stay under fieldpath/object
as they act as a bridge between runtime.Object
s and Paved
s. Maybe a little less true for object.MergeReplace
, but then even object.MergeReplace
accepts a path
parameter.
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.
Hrm I can see a case either way. Perhaps we should start with these in core Crossplane - we can move them here if we find our library of functions that operate on objects grows.
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've moved the functions in fieldpath.object
to core Crossplane including resource.WithMergeOptions
and unexported them. If we need to use them elsewhere, we can consider to export them but for now, no need to export them. Please see the dependent PR.
…is set - Move resource.WithMergeOptions to core Crossplane and unexport - Move fieldpath.object functions to core Crossplane and unexport - Move fieldpath.MergeValue & related functions to its own file - Add tests for fieldpath.MergeValue Signed-off-by: Alper Rifat Ulucinar <ulucinar@users.noreply.github.com>
Hi @negz, I realized that when Do you think we had better add a new |
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.
Thanks for persisting with this @ulucinar! One last thing, but otherwise this looks good to me.
My opinion is that we do not need unless a good use-case pops up.
I agree - let's wait for now.
Signed-off-by: Alper Rifat Ulucinar <ulucinar@users.noreply.github.com>
Description of your changes
Please refer to crossplane/crossplane#2211 for more background on this PR.
This PR aims to add merge support to the
fieldpath
package and moves some code from thecrossplane
repo to thecrossplane-runtime
repo to implement common functionality & prevent code duplication. For merging objects (map[string]interface{}
), the mergo library is used. Certain aspects of the behavior of themergo
library can be controlled via the newfieldpath.MergeOptions
struct. Merging of a value at a given path of the desiredruntime.Object
into the actual (current)runtime.Object
is achieved via aresource.ApplyOption
that's passed as an argument toresource.Applicator.Apply
. This new kind ofApplyOption
can be obtained using the newresource.WithMergeOptions
constructor.crossplane
repo PR crossplane/crossplane#2409 depends on this PR.I have:
make reviewable test
to ensure this PR is ready for review.How has this code been tested
Together with crossplane/crossplane#2409, I have manually tested this PR using the following manifests. I have kubectl edited resources to test & observe the behavior (e.g., when
keepMapValues
isfalse
or is missing in the composition for themetadata
patch, observe that claim's annotations overwrite composed's annotations with the same keys. However, whenkeepMapValues
is true, existing values in the composed annotations are preserved even if the claim (and correspondingly the XR) has a different value for the same key):