Skip to content
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

Merged
merged 3 commits into from
Nov 22, 2017

Conversation

mengqiy
Copy link
Member

@mengqiy mengqiy commented Aug 25, 2017

  • support openapi in strategic merge patch
  • test openapi in strategic merge patch
  • kubectl apply use openapi to calculate diff be default. It will fall back to use baked-in types when openapi is not available.
  • test openapi in kubectl apply

Fixes: kubernetes/kubectl#55

kubectl apply use openapi to calculate diff be default. It will fall back to use baked-in types when openapi is not available.

/assign @apelisse

@k8s-ci-robot k8s-ci-robot added size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. labels Aug 25, 2017
@k8s-github-robot k8s-github-robot added the release-note Denotes a PR that will be considered when it comes time to generate release notes. label Aug 25, 2017
@mengqiy
Copy link
Member Author

mengqiy commented Aug 25, 2017

I will add tests for kubectl apply soon. It should not affect other code.

Copy link
Member

@apelisse apelisse left a 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
Copy link
Member

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 @@
/*
Copy link
Member

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 :-)

Copy link
Member

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 {
Copy link
Member

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"})
Copy link
Member

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

Copy link
Member Author

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
Copy link
Member

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 {
Copy link
Member

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

Copy link
Member

@apelisse apelisse left a 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)
Copy link
Member

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},
Copy link
Member

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

Copy link
Member

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.

Copy link
Member

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)
Copy link
Member

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 :-/

Copy link
Member Author

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
Copy link
Member

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 {
Copy link
Member

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) {
Copy link
Member

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
Copy link
Member

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"})
Copy link
Member

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())
Copy link
Member

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?

Copy link
Member Author

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]
Copy link
Member

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())

@mengqiy mengqiy changed the title Kubectl apply openapi [WIP] Kubectl apply openapi Aug 28, 2017
@mengqiy mengqiy force-pushed the kubectl_apply_openapi branch 3 times, most recently from 46bd7c8 to 197316b Compare August 30, 2017 01:40
@k8s-reviewable
Copy link

This change is Reviewable

@mengqiy
Copy link
Member Author

mengqiy commented Aug 30, 2017

@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.
FYI, to review apply_test.go, using https://reviewable.kubernetes.io/reviews/kubernetes/kubernetes/51321 will be helpful.

@mengqiy mengqiy changed the title [WIP] Kubectl apply openapi Kubectl apply openapi Aug 30, 2017
@kubernetes kubernetes deleted a comment from mengqiy Aug 30, 2017
@kubernetes kubernetes deleted a comment from mengqiy Aug 30, 2017
Copy link
Member

@apelisse apelisse left a 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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove :-)

@@ -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.")
Copy link
Member

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?

Copy link
Member Author

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([
Copy link
Member

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",
Copy link
Member

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)}
Copy link
Member

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 {
Copy link
Member

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?

Copy link
Member Author

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.

@mengqiy
Copy link
Member Author

mengqiy commented Aug 30, 2017

@apelisse Bazel test is happy now. Thank you soooo much for helping me figure out the bazel problem!

@mengqiy mengqiy force-pushed the kubectl_apply_openapi branch 2 times, most recently from 518d2ff to 838d471 Compare August 31, 2017 21:34
@mengqiy mengqiy changed the title Kubectl apply openapi Kubectl apply and strategic merge patch using openapi Sep 1, 2017
@mengqiy mengqiy force-pushed the kubectl_apply_openapi branch 3 times, most recently from a7519f8 to 922aecc Compare September 1, 2017 07:29
@mengqiy
Copy link
Member Author

mengqiy commented Sep 1, 2017

@apelisse ping for review or lgtm

@mengqiy
Copy link
Member Author

mengqiy commented Nov 22, 2017

/priority critical-urgent
Label it as critical-urgent to prevent the bot from moving out of 1.9 milestone.

@k8s-ci-robot k8s-ci-robot added the priority/critical-urgent Highest priority. Must be actively worked on as someone's top priority right now. label Nov 22, 2017
@mengqiy
Copy link
Member Author

mengqiy commented Nov 22, 2017

/remove-priority important-soon

@k8s-ci-robot k8s-ci-robot removed the priority/important-soon Must be staffed and worked on either currently, or very soon, ideally in time for the next release. label Nov 22, 2017
@mengqiy
Copy link
Member Author

mengqiy commented Nov 22, 2017

@thockin Can you please approve the godep change in be20a67? They are auto generated by script.

@thockin
Copy link
Member

thockin commented Nov 22, 2017

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

@k8s-ci-robot k8s-ci-robot added sig/contributor-experience Categorizes an issue or PR as relevant to SIG Contributor Experience. kind/bug Categorizes issue or PR as related to a bug. labels Nov 22, 2017
@k8s-github-robot
Copy link

[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 /approve in a comment
You can cancel your approval by writing /approve cancel in a comment

@k8s-github-robot k8s-github-robot added approved Indicates a PR has been approved by an approver from all required OWNERS files. milestone/incomplete-labels and removed milestone/needs-attention labels Nov 22, 2017
@k8s-github-robot
Copy link

[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 kind/bug, kind/cleanup or kind/feature.

Help

@k8s-github-robot
Copy link

/test all [submit-queue is verifying that this PR is safe to merge]

@k8s-github-robot
Copy link

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.

@k8s-github-robot k8s-github-robot merged commit e412ad5 into kubernetes:master Nov 22, 2017
@k8s-ci-robot
Copy link
Contributor

k8s-ci-robot commented Nov 22, 2017

@mengqiy: The following tests failed, say /retest to rerun them all:

Test name Commit Details Rerun command
pull-kubernetes-bazel 29f6d0825e5a8c87bfa988058931b43012d162de link /test pull-kubernetes-bazel
pull-kubernetes-e2e-gce-gpu d4167cf044abfc5c2c551963c1467d1cad5dd16f link /test pull-kubernetes-e2e-gce-gpu
pull-kubernetes-e2e-gce-bazel d4167cf044abfc5c2c551963c1467d1cad5dd16f link /test pull-kubernetes-e2e-gce-bazel
pull-kubernetes-e2e-gce-etcd3 d4167cf044abfc5c2c551963c1467d1cad5dd16f link /test pull-kubernetes-e2e-gce-etcd3
pull-kubernetes-node-e2e be20a67 link /test pull-kubernetes-node-e2e

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.

@mengqiy mengqiy deleted the kubectl_apply_openapi branch November 27, 2017 21:41
// 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 {
Copy link
Member

@liggitt liggitt Nov 30, 2017

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.

Copy link
Member Author

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?

Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

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

Copy link
Member

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.

Copy link
Member

@apelisse apelisse Dec 1, 2017

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?

Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Contributor

@shiywang shiywang Dec 3, 2017

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

@liggitt
Copy link
Member

liggitt commented Dec 1, 2017 via email

k8s-github-robot pushed a commit that referenced this pull request Dec 4, 2017
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
approved Indicates a PR has been approved by an approver from all required OWNERS files. cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. kind/bug Categorizes issue or PR as related to a bug. kind/feature Categorizes issue or PR as related to a new feature. lgtm "Looks good to me", indicates that a PR is ready to be merged. milestone/incomplete-labels priority/critical-urgent Highest priority. Must be actively worked on as someone's top priority right now. release-note Denotes a PR that will be considered when it comes time to generate release notes. sig/api-machinery Categorizes an issue or PR as relevant to SIG API Machinery. sig/cli Categorizes an issue or PR as relevant to SIG CLI. sig/contributor-experience Categorizes an issue or PR as relevant to SIG Contributor Experience. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet