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

OpenAPI downloads protobuf rather than Json #46803

Merged
merged 1 commit into from Jun 30, 2017

Conversation

apelisse
Copy link
Member

@apelisse apelisse commented Jun 1, 2017

What this PR does / why we need it:
The current implementation of the OpenAPI getter fetches the swagger in a Json format from the apiserver. The Json file is big (~1.7mb), which means that it takes a long time to download, and then a long time to parse. Because that is going to be needed on each kubectl run later, we want this to be as fast as possible.

The apiserver has been modified to be able to return a protobuf version of the swagger, which this patch intends to use.

Note that there is currently no piece of code that exists that allows us to go from the protobuf version of the file, back into Json and/or spec.Swagger. Because the protobuf is not very different (but significantly different enough that it can't be translated), I've updated the code to use openapi_v2.Document (the protobuf type) everywhere rather than spec.Swagger. The behavior should be identical though.

There are more changes that are coming in follow-up pull-requests: using the gzip version (also provided by the new apiserver) to even further reduce the size of the downloaded content, and use the HTTP Etag cache mechanism to completely get rid of recurrent fetch requests. I'm currently working on these two features.

Which issue this PR fixes (optional, in fixes #<issue number>(, fixes #<issue_number>, ...) format, will close that issue when PR gets merged): partly #38637

Special notes for your reviewer:

Release note:

NONE

@k8s-ci-robot k8s-ci-robot added the cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. label Jun 1, 2017
@k8s-github-robot k8s-github-robot added size/L Denotes a PR that changes 100-499 lines, ignoring generated files. release-note-none Denotes a PR that doesn't merit a release note. labels Jun 1, 2017
@@ -98,7 +100,7 @@ type Kind struct {
PrimitiveType string

// Extensions are openapi extensions for the object definition.
Copy link
Member

Choose a reason for hiding this comment

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

Update the comment.

Copy link
Member Author

Choose a reason for hiding this comment

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

Should I?

Copy link
Member

Choose a reason for hiding this comment

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

Maybe not. They are the same the type under the cover.

}

func vendorExtensionToMap(e []*openapi_v2.NamedAny) map[string]interface{} {
var values map[string]interface{}
Copy link
Member

Choose a reason for hiding this comment

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

You can values := map[string]interface{}{}
and then remove

if values == nil {
  values = make(map[string]interface{})
}

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, that's exactly what I had done initially. Problem is that it broke many tests. Many tests build an expected Kind/Type with the default Extension value nil. Problem is, this function will always initialize Extension with an empty Extension, which doesn't compare as equal to nil in reflect.DeepEqual.

IOW, the goal of this if is to return a nil map rather than an empty map, so that the tests don't have to worry about initializing an empty map ...

@@ -202,13 +228,13 @@ func (o *Resources) parseDefinition(name string, s spec.Schema) Kind {
if o.isPrimitive(s) {
value.PrimitiveType = o.getTypeNameForField(s)
}
for fieldname, property := range s.Properties {
value.Fields[fieldname] = o.parseField(property)
for _, ns := range s.Properties.GetAdditionalProperties() {
Copy link
Member

Choose a reason for hiding this comment

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

Is it safer to use s.GetProperties().GetAdditionalProperties()?

Copy link
Member Author

Choose a reason for hiding this comment

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

Safer, no. More consistent with how I've done it throughout the file, yes.

I discussed that with Phillip and we pretty much agreed that null-checking everything was probably overkill

// Get the reference for complex types
if o.isDefinitionReference(s) {
return o.nameForDefinitionField(s)
}
// Recurse if type is array
if o.isArray(s) {
return fmt.Sprintf("%s array", o.getTypeNameForField(*s.Items.Schema))
return fmt.Sprintf("%s array", o.getTypeNameForField(s.GetItems().GetSchema()[0]))
Copy link
Member

Choose a reason for hiding this comment

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

Is it guaranteed to have at least one item?

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, but I think it falls under the same logic as above: "we assume we have tested this code against the swagger file before".

}
return *s.Items.Schema, nil
return s.GetItems().GetSchema()[0], nil
Copy link
Member

Choose a reason for hiding this comment

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

Same question here. Should we check the length of the schema slice?

empty := schema.GroupVersionKind{}

// Get the extensions
extList, f := s.Extensions[groupVersionKindExtensionKey]
extList, f := vendorExtensionToMap(s.GetVendorExtension())[groupVersionKindExtensionKey]
Copy link
Member

Choose a reason for hiding this comment

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

Maybe save the result of vendorExtensionToMap(s.GetVendorExtension()) to avoid process this again below.

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 idea

@@ -366,9 +391,9 @@ func (o *Resources) getGroupVersionKind(s spec.Schema) (schema.GroupVersionKind,
gvk := extListCasted[0]

// Expect a empty of a map with 3 entries
gvkMap, ok := gvk.(map[string]interface{})
gvkMap, ok := gvk.(map[interface{}]interface{})
Copy link
Member

Choose a reason for hiding this comment

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

Interesting. It is not a map[string]interface{} any more?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, seems like the new type uses a different format.

@@ -20,8 +20,10 @@ import (
"fmt"
"strings"

"github.com/go-openapi/spec"
yaml "gopkg.in/yaml.v2"
Copy link
Member

Choose a reason for hiding this comment

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

Not sure if we should use this or github.com/ghodss/yaml?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, probably doesn't matter, but it looks like yours is used slightly more

Copy link
Member

Choose a reason for hiding this comment

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

@fabianofranz Do you know what yaml pkg should we use?

Copy link
Member Author

Choose a reason for hiding this comment

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

Actually we don't have a choice. I use a feature from gopkg.in/yaml.v2

func (d *DiscoveryClient) OpenAPISchema() (*spec.Swagger, error) {
data, err := d.restClient.Get().AbsPath("/swagger.json").Do().Raw()
func (d *DiscoveryClient) OpenAPISchema() (*openapi_v2.Document, error) {
data, err := d.restClient.Get().AbsPath("/swagger-2.0.0.pb-v1").Do().Raw()
Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

@apelisse apelisse force-pushed the new-download-openapi branch 2 times, most recently from 85f9230 to d7ccf24 Compare June 2, 2017 18:03
Copy link
Contributor

@droot droot left a comment

Choose a reason for hiding this comment

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

looks good to me, a few minor comments.

"github.com/golang/glog"
openapi_v2 "github.com/googleapis/gnostic/OpenAPIv2"
Copy link
Contributor

Choose a reason for hiding this comment

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

wondering if we have any convention for package import names ? using "_" is a standard thing (I see metav1 below) ?

Copy link
Member Author

@apelisse apelisse Jun 2, 2017

Choose a reason for hiding this comment

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

This is mostly because my goimports was broken. I've fixed it and I'm using the default name now (which happens to also be openapi_v2 ;-)

definition := o.parseDefinition(name, d)
o.NameToDefinition[name] = definition
for _, ns := range doc.GetDefinitions().GetAdditionalProperties() {
definition := o.parseDefinition(ns.Name, ns.Value)
Copy link
Contributor

Choose a reason for hiding this comment

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

does ns have Getters for name and value. If yes, then we should use those to be consistent.

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, good catch!

@apelisse
Copy link
Member Author

apelisse commented Jun 2, 2017

Addressed! Please take a look!

@mengqiy
Copy link
Member

mengqiy commented Jun 2, 2017

@k8s-bot pull-kubernetes-unit test this
@k8s-bot pull-kubernetes-e2e-gce-etcd3 test this

@mengqiy
Copy link
Member

mengqiy commented Jun 2, 2017

/lgtm

@k8s-ci-robot k8s-ci-robot added the lgtm "Looks good to me", indicates that a PR is ready to be merged. label Jun 2, 2017
@mengqiy
Copy link
Member

mengqiy commented Jun 2, 2017

@k8s-bot pull-kubernetes-unit test this

@mbohlool
Copy link
Contributor

mbohlool commented Jun 3, 2017

apiserver. The Json file is big (~330mb), which means that it takes a long time to download, and then

330mb? it is 1.7MB. right?

But the point is valid that it takes much longer to unmarshal. Some numbers here: https://docs.google.com/document/d/1EcnvLRqP9-5mqQ-kghfnB3GtB8eIrkVQjn1B16NRrr0

@apelisse
Copy link
Member Author

apelisse commented Jun 3, 2017

330mb? it is 1.7MB. right?

I have no idea how I made up that number :D. I'll update the description.

@apelisse
Copy link
Member Author

apelisse commented Jun 5, 2017

@pwittrock Please take a look!

@pwittrock
Copy link
Member

/approve

@k8s-github-robot k8s-github-robot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Jun 6, 2017
@apelisse
Copy link
Member Author

I've added a new commit to download the gzipped protobuf.

@k8s-github-robot k8s-github-robot removed lgtm "Looks good to me", indicates that a PR is ready to be merged. needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. labels Jun 12, 2017
@pwittrock
Copy link
Member

@k8s-bot pull-kubernetes-e2e-gce-etcd3 test this

@k8s-github-robot k8s-github-robot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Jun 22, 2017
@k8s-github-robot k8s-github-robot removed the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Jun 23, 2017
@pwittrock
Copy link
Member

/lgtm

@k8s-ci-robot k8s-ci-robot added the lgtm "Looks good to me", indicates that a PR is ready to be merged. label Jun 26, 2017
@pwittrock
Copy link
Member

/approve

@apelisse apelisse assigned deads2k and unassigned droot, mengqiy and pwittrock Jun 26, 2017
data, err := d.restClient.Get().AbsPath("/swagger.json").Do().Raw()
// OpenAPISchema fetches the open api schema using a rest client and parses the proto.
func (d *DiscoveryClient) OpenAPISchema() (*openapi_v2.Document, error) {
stream, err := d.restClient.Get().AbsPath("/swagger-2.0.0.pb-v1.gz").Stream()
Copy link
Contributor

Choose a reason for hiding this comment

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

This looks off to me. Why wouldn't you request a gzipped encoding of the normal protobuf struct?

Copy link
Member Author

Choose a reason for hiding this comment

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

Because the server doesn't support it

Copy link
Contributor

Choose a reason for hiding this comment

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

Because the server doesn't support it

I'm pretty sure it merged recently here: #46966

Copy link
Member Author

Choose a reason for hiding this comment

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

This code is targeting this end-point, is it related?

@deads2k
Copy link
Contributor

deads2k commented Jun 26, 2017

This code is targeting this end-point, is it related?

The handler was supposed to be written as a "normal" http handler wrapper. Can you try wrapping it? I'd rather not enshrine a temporary condition in the API.

@apelisse
Copy link
Member Author

Yeah I'll look at it

@apelisse
Copy link
Member Author

OK I've just removed that part. Go will take care of that part if the server supports the gzip encoding.

@apelisse
Copy link
Member Author

Can you PTAL @deads2k

@mbohlool
Copy link
Contributor

If it is not too much trouble, can you separate generated/godep commits from others?

data, err := d.restClient.Get().AbsPath("/swagger.json").Do().Raw()
// OpenAPISchema fetches the open api schema using a rest client and parses the proto.
func (d *DiscoveryClient) OpenAPISchema() (*openapi_v2.Document, error) {
data, err := d.restClient.Get().AbsPath("/swagger-2.0.0.pb-v1").Do().Raw()
Copy link
Contributor

Choose a reason for hiding this comment

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

Ok, this now looks good apiserver-wise, but client-side it seems like this will break when run against old servers. Don't you need a fallback to the old swagger.json?

Copy link
Member Author

@apelisse apelisse Jun 28, 2017

Choose a reason for hiding this comment

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

It shouldn't. 1.7 server already supports that end-point, and this code is meant for 1.8 client.

Copy link
Contributor

Choose a reason for hiding this comment

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

It shouldn't. 1.7 server already supports that end-point, and this code is meant for 1.8 client.

I rather not have this be the thing that breaks generic commands (like create) from working. If it returns an error, will kubectl still work? If not, at least guarding it sufficiently to keep that working seems reasonable even if client side validation doesn't work.

Copy link
Member

Choose a reason for hiding this comment

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

If --validate=false, we shouldn't block on getting the openapi. If --validate=true, we should fail if we can't get the openapi to validate.

Copy link
Member Author

Choose a reason for hiding this comment

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

IOW, This is fine for now as this code is not even used for validation. The only way to actually exercise this code today is to use --experimental-use-openapi-print-columns.

Copy link
Contributor

Choose a reason for hiding this comment

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

In other words, I don't think kubectl of any version should ever use openapi with a pre-1.7 server version.

Given code to go from spec.Schema -> openapi_v2.Document, it seems like you could hit the old endpoint, read into spec.Schema (slow as it is), then convert that to the openapi_v2.Document to return.

I'm fine telling people they shouldn't use the feature against older servers. I'm less fine straight up breaking them. With --validate=true being the default, that's what would happen with a 1.8 kubectl and 1.6 server (which a client may not be able to update), right? Openapi for validation is a goal.

Copy link
Member Author

Choose a reason for hiding this comment

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

We could keep the code as it is, have it fail on validating with the openapi, and then automatically fallback on swagger. That would allow us even longer backward compatibility.

Copy link
Contributor

Choose a reason for hiding this comment

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

We could keep the code as it is, have it fail on validating with the openapi, and then automatically fallback on swagger. That would allow us even longer backward compatibility.

Sold. Is it practical do in this pull?

Copy link
Member Author

Choose a reason for hiding this comment

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

Nothing really uses this code so far, and I'm working on adding new features but it's too substantial to be added to this pr. I'll definitely add you as a reviewer/approver on pull-requests to come.

Copy link
Contributor

Choose a reason for hiding this comment

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

Nothing really uses this code so far, and I'm working on adding new features but it's too substantial to be added to this pr. I'll definitely add you as a reviewer/approver on pull-requests to come.

Ok. Please open an issue for yourself about maintaining compatibility.

@deads2k
Copy link
Contributor

deads2k commented Jun 30, 2017

I'm ok with an issue that you'll fix in 1.8 to maintain compatibility.

/approve

@k8s-github-robot
Copy link

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: apelisse, deads2k, mengqiy, pwittrock

Associated issue: 38637

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 the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Jun 30, 2017
@apelisse
Copy link
Member Author

/retest

@k8s-github-robot
Copy link

Automatic merge from submit-queue (batch tested with PRs 43558, 48261, 42376, 46803, 47058)

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. lgtm "Looks good to me", indicates that a PR is ready to be merged. release-note-none Denotes a PR that doesn't merit a release note. size/L Denotes a PR that changes 100-499 lines, ignoring generated files.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants