-
Notifications
You must be signed in to change notification settings - Fork 210
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
yaml unmarshal for OpenAPIv2 types #279
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,8 +17,11 @@ limitations under the License. | |
package util | ||
|
||
import ( | ||
"fmt" | ||
"reflect" | ||
"strings" | ||
|
||
"gopkg.in/yaml.v3" | ||
) | ||
|
||
// [DEPRECATED] ToCanonicalName converts Golang package/type canonical name into REST friendly OpenAPI name. | ||
|
@@ -108,3 +111,21 @@ func GetCanonicalTypeName(model interface{}) string { | |
} | ||
return path + "." + t.Name() | ||
} | ||
|
||
// Provides a fast path for decoding YAML scalar node as a string | ||
// If the node's value can be simply returned directly, then it is. Otherwise, | ||
// the yaml.v3.Node.Decode slow path is taken | ||
func DecodeYAMLString(n *yaml.Node, s *string) error { | ||
if n.Kind != yaml.ScalarNode { | ||
return fmt.Errorf("expected scalarnode, not %v", n.Kind) | ||
} | ||
|
||
if (n.Tag == "!!str" || n.Tag == "tag:yaml.org,2002:!!string") || | ||
(n.Tag == "" || n.Tag == "!") && n.Style&(yaml.SingleQuotedStyle|yaml.DoubleQuotedStyle|yaml.LiteralStyle|yaml.FoldedStyle) != 0 { | ||
Comment on lines
+123
to
+124
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this copied from something in the yaml library? I'm not familiar with what this means or if it is correct There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I copied it from yaml decoding logic for string path: https://github.com/go-yaml/yaml/blob/496545a6307b2a7d7a710fd516e5e16e8ab62dbc/yaml.go#L463 |
||
*s = n.Value | ||
return nil | ||
} | ||
|
||
// Use slow path if it is not a basic string | ||
return n.Decode(&s) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,7 +18,7 @@ package spec | |
// | ||
// For more information: http://goo.gl/8us55a#contactObject | ||
type ContactInfo struct { | ||
Name string `json:"name,omitempty"` | ||
URL string `json:"url,omitempty"` | ||
Email string `json:"email,omitempty"` | ||
Name string `json:"name,omitempty" yaml:"name,omitempty"` | ||
URL string `json:"url,omitempty" yaml:"url,omitempty"` | ||
Email string `json:"email,omitempty" yaml:"email,omitempty"` | ||
Comment on lines
+21
to
+23
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I really don't think we should start adding yaml tags to our API types. Having benchmarked the yaml decoding in the past, I'm also really surprised it is faster than the json decoding (it was generally significantly slower in the past). Do we know which bits the json decoding is super slow on? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yaml package since v3 released 2019 operates on an AST; did you look before then? My benchmarks measured unmarshalling from the AST type instead of text. This is sufficient for my use case of providing a path to convert from protobuf: I haven't benchmarked it, but I expect going from YAML text -> AST -> Kube-Openapi would be comparable/slower than JSON. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we want a google/gnostic → kube-openapi path, it would make more sense to me to build that transformation and round-trip test it. Using yaml AST bits is clever, but I don't think we should decorate API types with yaml tags and push consumers in that direction There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I'm curious why you're against having yaml tags here? The proto->gnostic->kube-openapi conversion that this facilitates is a critical performance improvement that kpt and kustomize would like to have as soon as possible, and I'm not sure that I understand the disadvantages of adding yaml tags. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We have an alternative solution that is probably going to be even faster, but it will take a few weeks to implement. This is also a blocker for next code-freeze so we're trying to move fast on that! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you so much for the update! |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,9 +16,12 @@ package spec | |
|
||
import ( | ||
"encoding/json" | ||
"errors" | ||
"strings" | ||
|
||
"github.com/go-openapi/swag" | ||
"gopkg.in/yaml.v3" | ||
"k8s.io/kube-openapi/pkg/util" | ||
) | ||
|
||
// Extensions vendor specific extensions | ||
|
@@ -133,14 +136,43 @@ func (v *VendorExtensible) UnmarshalJSON(data []byte) error { | |
return nil | ||
} | ||
|
||
func (v *VendorExtensible) UnmarshalYAML(value *yaml.Node) error { | ||
// var d map[string]interface{} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this line needed? |
||
if value.Kind != yaml.MappingNode { | ||
return errors.New("expected mappingNode") | ||
} else if len(value.Content)%2 != 0 { | ||
return errors.New("expected even child nodes") | ||
} | ||
|
||
for i := 0; i < len(value.Content); i += 2 { | ||
var keyStr string | ||
if err := util.DecodeYAMLString(value.Content[i], &keyStr); err != nil { | ||
return err | ||
} | ||
|
||
if strings.HasPrefix(keyStr, "x-") || strings.HasPrefix(keyStr, "X-") { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is the capitalization check capturing an edge case or do we already have places where There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This extra check for "X-" is here only to mirror the UnmarshalJSON behavior. I think you wrote that ;) (Unmarshal JSON calls |
||
if v.Extensions == nil { | ||
v.Extensions = map[string]interface{}{} | ||
} | ||
|
||
var decodedVal interface{} | ||
if err := value.Content[i+1].Decode(&decodedVal); err != nil { | ||
return err | ||
} | ||
v.Extensions[strings.ToLower(keyStr)] = decodedVal | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
// InfoProps the properties for an info definition | ||
type InfoProps struct { | ||
Description string `json:"description,omitempty"` | ||
Title string `json:"title,omitempty"` | ||
TermsOfService string `json:"termsOfService,omitempty"` | ||
Contact *ContactInfo `json:"contact,omitempty"` | ||
License *License `json:"license,omitempty"` | ||
Version string `json:"version,omitempty"` | ||
Description string `json:"description,omitempty" yaml:"description,omitempty"` | ||
Title string `json:"title,omitempty" yaml:"title,omitempty"` | ||
TermsOfService string `json:"termsOfService,omitempty" yaml:"termsOfService,omitempty"` | ||
Contact *ContactInfo `json:"contact,omitempty" yaml:"contact,omitempty"` | ||
License *License `json:"license,omitempty" yaml:"license,omitempty"` | ||
Version string `json:"version,omitempty" yaml:"version,omitempty"` | ||
} | ||
|
||
// Info object provides metadata about the API. | ||
|
@@ -172,3 +204,10 @@ func (i *Info) UnmarshalJSON(data []byte) error { | |
} | ||
return json.Unmarshal(data, &i.VendorExtensible) | ||
} | ||
|
||
func (i *Info) UnmarshalYAML(value *yaml.Node) error { | ||
if err := value.Decode(&i.InfoProps); err != nil { | ||
return err | ||
} | ||
return i.VendorExtensible.UnmarshalYAML(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.
What's the performance difference with this change? I'm curious if this might be better committed in the YAML library rather than here since there isn't any k8s specific logic.
/cc @apelisse
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.
+1 on not putting general-purpose yaml decoding in this lib
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.
Agree that it is not desirable long-term. I will prepare a PR for upstream. However, I do not see an alternative in the short term? It will likely take a while for the change to be merged and appear in a tagged 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.
@Jefftree The difference with this change is significant. Below are benchmark results for openapi v2 with this optimization removed:
~93ms in this run on my machine. Over a number of runs i saw a range of 80-100ms for yaml->swagger.
This is compared to ~60ms with the optimization enabled.