-
Notifications
You must be signed in to change notification settings - Fork 38.6k
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
Kubectl apply and strategic merge patch using openapi #51321
Kubectl apply and strategic merge patch using openapi #51321
Conversation
I will add tests for kubectl apply soon. It should not affect other 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.
That is very big, and there are clear ways to split. Would you mind splitting in smaller pieces?
@@ -111,23 +111,23 @@ func parseGroupVersionKind(s *openapi_v2.Schema) schema.GroupVersionKind { | |||
// Definitions is an implementation of `Resources`. It looks for | |||
// resources in an openapi Schema. | |||
type Definitions struct { | |||
models map[string]Schema |
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.
These should really really not be public. Let's see below why you need that.
@@ -0,0 +1,43 @@ | |||
/* |
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 should be done in a preliminary pull-request, along with a description. It's really hard to guess what you're trying to do and why you're doing this. (though I might have an idea :-)
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 maybe move that code to a different package?
type baseItem struct { | ||
errors Errors | ||
path openapi.Path | ||
type baseValidationItem struct { |
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.
do we need that type if it's basically just embedding the openapi.BaseItem
?
@@ -81,11 +63,11 @@ func (item *mapItem) sortedKeys() []string { | |||
var _ ValidationItem = &mapItem{} | |||
|
|||
func (item *mapItem) VisitPrimitive(schema *openapi.Primitive) { | |||
item.AddValidationError(InvalidTypeError{Path: schema.GetPath().String(), Expected: schema.Type, Actual: "map"}) | |||
item.AddValidationError(openapi.InvalidTypeError{Path: schema.GetPath().String(), Expected: schema.Type, Actual: "map"}) |
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.
InvalidTypeError
is a validation error, I'm not sure why it's coming from openapi
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 reusing it in my code.
} | ||
|
||
// mapItem represents a map entry in the yaml. | ||
type mapItem struct { | ||
baseItem | ||
baseValidationItem |
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.
do we need that type if it's basically just embedding the openapi.BaseItem?
Then this would be openapi.BaseItem
// Errors returns the list of errors found for this item. | ||
func (item *baseItem) Errors() []error { | ||
return item.errors.Errors() | ||
func NewBaseValidationItem(path openapi.Path) baseValidationItem { |
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.
do we need that type if it's basically just embedding the openapi.BaseItem?
If we don't have the extra layer, then this function can be removed
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 still haven't read patch.go
|
||
errs := patchItem.Errors() | ||
if errs != nil { | ||
return nil, nil, utilerrors.NewAggregate(errs) |
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.
Looking at PatchMetaFromStruct.LookupPatchMetadata
, it looks like aggregating the errors is a new feature. is this really something we want? IOW, it made sense for validation, does it make sense here? I'd rather keep it simple.
if errs != nil { | ||
return nil, nil, utilerrors.NewAggregate(errs) | ||
} | ||
return PatchMetaFromOpenAPI{Schema: patchItem.subschema}, |
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 don't know what subschema
is yet, but that looks a little odd
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.
OK, Now that I've seen what subschema is. I think that code belongs to types
. IOW, the code that depends on the subschema
should be where you know if you have a subschema
, a list
or a primitive
.
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.
was this addressed?
} | ||
|
||
func (s PatchMetaFromOpenAPI) Elem() LookupPatchMeta { | ||
arr, ok := s.Schema.(*openapi.Array) |
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 really shouldn't have to cast :-/
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 know cast is bad, when it supposed to be visited by the interface. :-/
I will try to refactor it and I will touch k8s.io/apimachinery/third_party/forked/golang/json/fields.go
to get rid of Elem()
.
type LookupPatchItem interface { | ||
openapi.SchemaVisitor | ||
|
||
Errors() []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.
As mentionned earlier, I'd maybe keep that as a simple Error error
.
Also note that if you make that change, everything will break in an non-obvious way because this doesn't implement a "Item" interface of some sort.
Path() *openapi.Path | ||
} | ||
|
||
type baseLookupPatchMetaItem struct { |
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.
Same as before, not sure this is super useful
*openapi.BaseItem | ||
} | ||
|
||
func (item *baseLookupPatchMetaItem) AddLookupPatchMetaError(err 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 don't think this should be necessary
var _ LookupPatchItem = &patchItem{} | ||
|
||
func (item *patchItem) VisitPrimitive(schema *openapi.Primitive) { | ||
item.subschema = 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.
oh I see, I'll comment up-there
} | ||
|
||
func (item *patchItem) VisitArray(schema *openapi.Array) { | ||
item.AddLookupPatchMetaError(openapi.InvalidTypeError{Path: schema.GetPath().String(), Expected: "array", Actual: "kind"}) |
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 would consider panicking here, assuming that validation would have caught that before. (and everywhere)
if s.Schema == nil { | ||
return nil, &PatchMeta{}, nil | ||
} | ||
patchItem := NewPatchItem(key, s.Schema.GetPath()) |
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.
key
is odd, do you always have a key
? what if you have an array
?
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.
Array should be handled by Elem().
} | ||
|
||
extensions := subschema.GetExtensions() | ||
ps := extensions[patchStrategyOpenapiextensionKey] |
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.
Maybe move that to a different function: parseStrategy(subschema.GetExtensions())
fa8caaa
to
29f6d08
Compare
46bd7c8
to
197316b
Compare
197316b
to
b87a3ba
Compare
@apelisse Addressed most of your comments except the bazel one. I have tweak the bazel BUILD files for a while. I can't make it work. Please advice. |
b87a3ba
to
ba2960a
Compare
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.
That is vastly better than the previous version, thank you Mengqi
@@ -1296,21 +1298,23 @@ func mergeMap(original, patch map[string]interface{}, t reflect.Type, mergeOptio | |||
} | |||
// If they're both maps or lists, recurse into the value. | |||
// First find the fieldPatchStrategy and fieldPatchMergeKey. | |||
fieldType, fieldPatchStrategies, fieldPatchMergeKey, err := forkedjson.LookupPatchMetadata(t, k) | |||
//fmt.Printf("schema: %#v\n", schema.(PatchMetaFromStruct).T) |
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.
Remove :-)
pkg/kubectl/cmd/apply.go
Outdated
@@ -127,6 +128,7 @@ func NewCmdApply(baseName string, f cmdutil.Factory, out, errOut io.Writer) *cob | |||
cmd.Flags().StringVarP(&options.Selector, "selector", "l", "", "Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2)") | |||
cmd.Flags().Bool("all", false, "Select all resources in the namespace of the specified resource types.") | |||
cmd.Flags().StringArray("prune-whitelist", []string{}, "Overwrite the default whitelist with <group/version/kind> for --prune") | |||
cmd.Flags().Bool("openapi-patch", true, "If true, use openapi rather than baked-in types when calculating diff.") |
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.
Do we fall back on "baked-in" if openapi is not present or doesn't have the merge info?
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 we do. I will update the help text.
|
||
filegroup( | ||
name = "testing-openapi-spec", | ||
srcs = glob([ |
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.
You don't need glob
.
|
||
var ( | ||
mergeItemGVK = schema.GroupVersionKind{ | ||
Group: "fake-group", |
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.
:D
var mergeItem MergeItem | ||
var ( | ||
mergeItem MergeItem | ||
mergeItemStructSchema PatchMetaFromStruct = PatchMetaFromStruct{T: GetTagStructTypeOrDie(mergeItem)} |
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.
You don't need to specify the type do you?
Can you just do
var (
// ...
mergeItemStructSchema = PatchMetaFromStruct{T: GetTagStructTypeOrDie(mergeItem)}
)
if !reflect.DeepEqual(got, expected) { | ||
t.Errorf("error in test case: %s\ncannot sort object:\n%s\nexpected:\n%s\ngot:\n%s\n", | ||
c.Description, mergepatch.ToYAMLOrError(c.Original), mergepatch.ToYAMLOrError(c.Sorted), jsonToYAMLOrError(got)) | ||
for _, schema := range schemas { |
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 that test fails, how do I know from the error message which variant of the schema failed?
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 point. It worth pointing out.
@apelisse Bazel test is happy now. Thank you soooo much for helping me figure out the bazel problem! |
518d2ff
to
838d471
Compare
a7519f8
to
922aecc
Compare
@apelisse ping for review or lgtm |
/priority critical-urgent |
/remove-priority important-soon |
Rather than going to top-level OWNERs, we should see approval from OWNERs of those subsystem that are not yet covered. @grodrigues3 @kubernetes/sig-contributor-experience-bugs this is an opportunity to refine the approver suggestions. /approve |
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: apelisse, liggitt, mengqiy, pwittrock, thockin Associated issue: 55 The full list of commands accepted by this bot can be found here.
Needs approval from an approver in each of these OWNERS Files:
You can indicate your approval by writing |
[MILESTONENOTIFIER] Milestone Pull Request Labels Incomplete @apelisse @mengqiy @pwittrock @smarterclayton @sttts Action required: This pull request requires label changes. kind: Must specify exactly one of |
/test all [submit-queue is verifying that this PR is safe to merge] |
Automatic merge from submit-queue (batch tested with PRs 51321, 55969, 55039, 56183, 55976). If you want to cherry-pick this change to another branch, please follow the instructions here. |
@mengqiy: The following tests failed, say
Full PR test history. Your PR dashboard. Please help us cut down on flakes by linking to an open issue when you hit one in your PR. Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository. I understand the commands that are listed here. |
// Try to use openapi first if the openapi spec is available and can successfully calculate the patch. | ||
// Otherwise, fall back to baked-in types. | ||
if p.openapiSchema != nil { | ||
if schema = p.openapiSchema.LookupResource(p.mapping.GroupVersionKind); schema != 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 there any indication in the discovery doc that the resource supports strategic merge patch? I would not assume the mere presence of an openapi schema to mean SMP is supported.
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 guess this question is related to the discussion #56606 (comment).
is there any indication in the discovery doc that the resource supports strategic merge patch?
I don't think there is an explicit indicator.
If there are patch metadata (e.g. PatchStrategy, MergeKey) in the discovery doc, then this type supports SMP. But if there is not patch metadata, the type may still support SMP and it uses the default behavior(replacing lists).
I would not assume the mere presence of an openapi schema to mean SMP is supported.
It's not clear what does SMP is supported
mean.
If a type and its schema appear in the openapi schema with no patch metadada, SMP will process it with the default approach (replacing a list). It should not do anything wrong.
Did I miss anything?
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 assumes every API server that publishes openapi schemas is able to accept strategic merge patches. Previously, the client only assumed that compiled-in known core kubernetes types were served by a server supporting SMP.
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.
Let me summarize it for SMP
Before this PR:
Only compiled-in types (native k8s api resources) are supported.
No support for user-defined api resource no support for CRD.
After this PR:
There are 2 approaches:
Using baked-in types: only support native k8s api resources
Using openapi schema: support native k8s api resources and user-defined api resource. But no support for CRD.
Correct me if anything is wrong.
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.
Using openapi schema: support native k8s api resources and user-defined api resource
It assumes user-defined api resources support strategic merge patch
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 to do MIME type negotiation, we need to parse the endpoints part of openapi schema.
If we want to use openapi docs dynamically to form patch requests, I think that's what we need to do.
So this way will require much more efforts than the approach I mentioned above.
IMO it may be risky to introduce a lot of new code at this point.
I agree it is more effort, but I have a hard time accepting the "if at first you don't succeed" negotiation approach when we've already been given exactly the information we need to know what patch types are accepted
and is risky at this point
I also agree with this. Which is better at this point for this release?
- attempting multiple patch types falling back to merge patch if SMP is rejected with 406?
- defaulting this feature off
- disabling this feature
Whatever we choose for the moment, we should work to make use of the accepted content in the next release.
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.
Can we: send SMP if the type is registered, JMP otherwise? AFAIU that wouldn't really change the existing behavior (before that pull-request), yet exercise that code and already have some benefits?
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 a good example of why we need libraries that combine the discovery and openapi information for each resource type. We need and abstraction that hides the specific source of metadata for the resources.
@liggitt What do you recommend for 1.9? Disable or try to get a fix in?
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 leaning to what @apelisse suggested in #51321 (comment).
It should be a trivial change with fewer than 50 lines of code change.
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 @liggitt @mengqiy about the MIME type negotiation part is that means we check the "consumes" portion of a related resource in discovery doc, if it support, then use SMP, if not use JMP. if so what we need maybe in 1.10 is to add the function as @mengqiy memtioned
But to do MIME type negotiation, we need to parse the endpoints part of openapi schema.
There is no facility to support parsing the endpoints part of openapi schema, we only have the code to parse the models part.
so all we have to do is make NewOpenAPIData
also return paths
and add some lookup/indication function ? @mengqiy @apelisse I would like to take a look of adding this openapi helper function if you haven't yet.
This is a good example of why we need libraries that combine the discovery and openapi information for each resource type. We need and abstraction that hides the specific source of metadata for the resources.
This also sounds interesting, but I haven't got the idea of "combine" @pwittrock could you explain more ? thanks
Using openapi if available to compute SMP for compiled in types seems like
the best we can do for 1.9 (known types can assume SMP as today, and just
get more up to date schema info via SMP)
In 1.10, we can expand using openapi for SMP for non-compiled-in types once
we add support for checking the "consumes" portion of the openapi doc
On Dec 1, 2017, at 1:59 PM, Phillip Wittrock <notifications@github.com> wrote:
*@pwittrock* commented on this pull request.
------------------------------
In pkg/kubectl/cmd/apply.go
<#51321 (comment)>:
createPatchErrFormat := "creating patch with:\noriginal:\n%s\nmodified:\n%s\ncurrent:\n%s\nfor:"
- switch {
- case runtime.IsNotRegisteredError(err):
- // fall back to generic JSON merge patch
- patchType = types.MergePatchType
- preconditions :=
[]mergepatch.PreconditionFunc{mergepatch.RequireKeyUnchanged("apiVersion"),
- mergepatch.RequireKeyUnchanged("kind"),
mergepatch.RequireMetadataKeyUnchanged("name")}
- patch, err = jsonmergepatch.CreateThreeWayJSONMergePatch(original,
modified, current, preconditions...)
- if err != nil {
- if mergepatch.IsPreconditionFailed(err) {
- return nil, nil, fmt.Errorf("%s", "At least one of apiVersion,
kind and name was changed")
+ // Try to use openapi first if the openapi spec is available and can
successfully calculate the patch.
+ // Otherwise, fall back to baked-in types.
+ if p.openapiSchema != nil {
+ if schema = p.openapiSchema.LookupResource(p.mapping.GroupVersionKind);
schema != nil {
This is a good example of why we need libraries that combine the discovery
and openapi information for each resource type. We need and abstraction
that hides the specific source of metadata for the resources.
@liggitt <https://github.com/liggitt> What do you recommend for 1.9?
Disable or try to get a fix in?
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#51321 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AA70cjQx3dpLJVpDu5lNuxGx_pjGX6f6ks5s8EypgaJpZM4PCTsm>
.
|
Automatic merge from submit-queue (batch tested with PRs 52013, 56719). If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>. Build patch from openapi only for registered types Address the concern in #51321 (review). fixes kubernetes/kubectl#156 ```release-note NONE ``` /assign @apelisse
Fixes: kubernetes/kubectl#55
/assign @apelisse