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

Add direct conversion from Gnostic v2 types to spec.Swagger #283

Merged
merged 1 commit into from
Apr 1, 2022

Conversation

alexzielenski
Copy link
Contributor

@alexzielenski alexzielenski commented Mar 9, 2022

This PR continues conversation from #279 and privately with @liggitt wherein we decided the best path to convert gnostic to kube-openapi was to write a direct conversion.

This PR adds a direct conversion from gnostic openapiv2 types to kube-openapi's spec.Swagger. Below are benchmark results from the same benchmark used in #279:

BenchmarkGnosticConversion/json->swagger
BenchmarkGnosticConversion/json->swagger-8                  2     537177100 ns/op    95737132 B/op    1381637 allocs/op
BenchmarkGnosticConversion/swagger->json
BenchmarkGnosticConversion/swagger->json-8                  9     125024798 ns/op    71075921 B/op     274071 allocs/op
BenchmarkGnosticConversion/json->gnostic
BenchmarkGnosticConversion/json->gnostic-8                  5     229392000 ns/op    81446859 B/op    1248395 allocs/op
BenchmarkGnosticConversion/gnostic->pb
BenchmarkGnosticConversion/gnostic->pb-8                  100      11095008 ns/op     2899968 B/op          1 allocs/op
BenchmarkGnosticConversion/gnostic->yaml
BenchmarkGnosticConversion/gnostic->yaml-8                 42      28717820 ns/op    32855169 B/op     264562 allocs/op

BenchmarkGnosticConversion/pb->gnostic
BenchmarkGnosticConversion/pb->gnostic-8                   97      13012220 ns/op     9480701 B/op     123829 allocs/op
BenchmarkGnosticConversion/gnostic->swagger
BenchmarkGnosticConversion/gnostic->swagger-8              50      25910391 ns/op    22287938 B/op     164414 allocs/op

pb->gnostic->swagger using this method takes ~39ms

this compares favorably to pb->gnostic->yaml->swagger from the last PR in ~97ms

which compared favorably to pb->gnostic->yaml->json->swagger, today's existing solution, which clocked this benchmark at ~628ms

Caveats:

  • Fields like Maximum, Minimum, MaxItems, etc in gnostic are not implemented as pointers. This means the conversion has no way to differentiate between one of these being 0 vs being unused
  • Kube-Openapi Swagger type has a number of fields added in OpenAPI v3, despite the fact it represents an OpenAPI v2 object. This fields cannot be populated by the unmarshaler since gnostic errors/ignores unrecognized keys. Thus, if one is roundtripping with gnostic via kube->json->gnostic->kube, those fields would be missing, since json->gnostic would ignore them.
  • spec.SchemaOrArray in gnostic's implementation only scans for one element, so successive schemas are ignored
  • A number of our kube-openapi Swagger types do not have VendorExtensible embedded even when OpenAPI spec permits "x-" extensions. This means any extensions used in gnostic for a few types are not carried over. This can be corrected in another PR
  • Response.Description and Parameter.Description are missing from our type definitions despite being available in openapi v2, so the information is dropped when converted from gnostic. This can be corrected in another PR.

For the use case of kubernetes' swagger.json, most of these caveats do not apply:

@natasha41575
/cc @apelisse

@k8s-ci-robot k8s-ci-robot requested a review from apelisse March 9, 2022 18:46
@k8s-ci-robot k8s-ci-robot added cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. labels Mar 9, 2022
@alexzielenski alexzielenski changed the title Add FromGnostic to spec.* types to convert gnostic openapiv2 Document to spec.Swagger Add direct conversion from Gnostic v2 types to spec.Swagger Mar 9, 2022
@liggitt
Copy link
Member

liggitt commented Mar 9, 2022

numbers look really promising, thanks

is this ready for review? I see a lot of question marks and commented out code

@alexzielenski
Copy link
Contributor Author

@liggitt It is ready for review as of my latest force push. You looked too quickly!

@alexzielenski alexzielenski force-pushed the gnostic_conversion branch 2 times, most recently from f98efd5 to f9d96bf Compare March 9, 2022 21:36
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.

Minor nits, the conversion code seems good to me, especially as it round-trips properly.

Comment on lines 348 to 630
func TestGnosticConversionSmallDeterministic2(t *testing.T) {
// A failed case of TestGnosticConversionSmallRandom
// which failed during development/testing loop
gnosticCommonTest(
t,
fuzz.
NewWithSeed(1646770841).
NilChance(0.8).
MaxDepth(10).
NumElements(1, 2),
)
}

func TestGnosticConversionSmallDeterministic3(t *testing.T) {
// A failed case of TestGnosticConversionSmallRandom
// which failed during development/testing loop
gnosticCommonTest(
t,
fuzz.
NewWithSeed(1646772024).
NilChance(0.8).
MaxDepth(10).
NumElements(1, 2),
)
}

func TestGnosticConversionSmallDeterministic4(t *testing.T) {
// A failed case of TestGnosticConversionSmallRandom
// which failed during development/testing loop
gnosticCommonTest(
t,
fuzz.
NewWithSeed(1646791953).
NilChance(0.8).
MaxDepth(10).
NumElements(1, 2),
)
}


func TestGnosticConversionSmallRandom(t *testing.T) {
seed := time.Now().Unix()
t.Log("Using seed: ", seed)

gnosticCommonTest(
t,
fuzz.
NewWithSeed(seed).
NilChance(0.8).
MaxDepth(10).
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 if we need all these variants of the test. If it were me, I'd have kept it in a single test, and ran a loop a few times (100? depending on how much each invocation takes), re-fuzzing every-time rather than sticking to either pre-determined seed or even changing parameters like this. I don't think the "size" really has an impact for these.

Copy link
Contributor Author

@alexzielenski alexzielenski Mar 10, 2022

Choose a reason for hiding this comment

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

I disagree that the "deterministic" tests add no value. The SmallRandom test case does as you described to fuzz for new failing test cases (though it maybe should be modified to fuzz more than once). Once a failing case is discovered by the random fuzzer it is easy to take the seed used to reproduce the failure, fix it, and add a regression test. Do this enough times and you have a corpus of tricky test cases the code is known to have failed in the past against to guard against regression.

To fuzz in a loop 100 times without printing the random seed or fuzzed object used for each case offers no value to a developer other than to say "it's broken", since the exact case can't be reproduced.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

even changing parameters like this. I don't think the "size" really has an impact for these.

I was forced to change the parameters since the fuzz to generate the object would take too long otherwise. The different "sizes" was useful for me during development to work on smaller cases and then larger and larger. The distinction may not be useful as part of the test sweet but I see no need to remove them.

pkg/validation/spec/gnostic_test.go Show resolved Hide resolved
pkg/validation/spec/gnostic_test.go Outdated Show resolved Hide resolved
@liggitt
Copy link
Member

liggitt commented Mar 10, 2022

Had a few comments:

  • zero-value handling for scalars (might be a limitation of gnostic, which is unfortunate... is that causing problems elsewhere?)
  • null vs empty handling of lists/maps
  • null pointer checks when ranging over lists of pointers
  • tracking/propagating whether unexpected data loss occurred

I didn't review the specific gnostic structs carefully to check all the fields were caught... I'm assuming fuzzing will catch simple field copies.

For the interface types we're doing type switches on, do you have the unit test coverage handy to see how well the tests are exercising all the cases?

@alexzielenski alexzielenski force-pushed the gnostic_conversion branch 3 times, most recently from a37ac22 to a9971bd Compare March 10, 2022 19:50
@alexzielenski
Copy link
Contributor Author

alexzielenski commented Mar 10, 2022

@liggitt With my latest updates I think I have addressed most of your comments:

  1. Zero-handling: Unfortunately there is no way to differentiate 0 with unspecified here. I also looked into the protoreflect api, and even that has logic for treating 0 as not being present. A shame. I haven't seen mention of this causing problems for anyone else, but I also haven't looked very hard.
  2. I've reviewed the creation of lists/maps to make the handling of empty lists/maps consistent: if gnostic has nil, kube-openapi will also use nil. And vice versa.
  3. I've also reviewed pointer usage especially regarding ranges over lists/maps of pointers. Appropriate guards are now in place.
  4. A new ok result has been added to track whether data loss was detected.

I gathered test coverage info for the gnostic tests. gnostic.go is currently 68.8% covered. I'm looking into increasing this. Upon a quick glance, at least not all of the type cases of Parameter are being exercised.

@liggitt
Copy link
Member

liggitt commented Mar 10, 2022

  1. Zero-handling: Unfortunately there is no way to differentiate 0 with unspecified here. I also looked into the protoreflect api, and even that has logic for treating 0 as not being present. A shame. I haven't seen mention of this causing problems for anyone else, but I also haven't looked very hard.

huh, that's a pretty sharp edge. I can easily imagine someone using maximum: 0. not sure what to do about that other than document it, and make sure authoritative validators don't use this method?

  1. I've reviewed the creation of lists/maps to make the handling of empty lists/maps consistent: if gnostic has nil, kube-openapi will also use nil. And vice versa.

👍

  1. I've also reviewed pointer usage especially regarding ranges over lists/maps of pointers. Appropriate guards are now in place.

👍

  1. A new ok result has been added to track whether data loss was detected.

👍

I gathered test coverage info for the gnostic tests. gnostic.go is currently 68.8% covered. I'm looking into increasing this. Upon a quick glance, at least not all of the type cases of Parameter are being exercised.

👍

@apelisse
Copy link
Member

huh, that's a pretty sharp edge. I can easily imagine someone using maximum: 0. not sure what to do about that other than document it, and make sure authoritative validators don't use this method?

Yeah, I think that's our only option right now. That's a little unfortunate but hopefully that'll be limited to OpenAPI v2.

@k8s-ci-robot k8s-ci-robot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Mar 14, 2022
@k8s-ci-robot k8s-ci-robot removed the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Mar 14, 2022
@alexzielenski
Copy link
Contributor Author

With my latest commit all supported gnostic types and significant LOC are now being exercised within the tests.

@alexzielenski
Copy link
Contributor Author

/retest

@k8s-ci-robot
Copy link
Contributor

@alexzielenski: Cannot trigger testing until a trusted user reviews the PR and leaves an /ok-to-test message.

In response to this:

/retest

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.

go.mod Show resolved Hide resolved
pkg/validation/spec/gnostic.go Show resolved Hide resolved
pkg/validation/spec/gnostic.go Outdated Show resolved Hide resolved
pkg/validation/spec/gnostic.go Outdated Show resolved Hide resolved
pkg/validation/spec/gnostic.go Outdated Show resolved Hide resolved
pkg/validation/spec/gnostic.go Outdated Show resolved Hide resolved
pkg/validation/spec/gnostic.go Show resolved Hide resolved
pkg/validation/spec/gnostic.go Show resolved Hide resolved
pkg/validation/spec/gnostic.go Show resolved Hide resolved
pkg/validation/spec/gnostic.go Show resolved Hide resolved
@liggitt
Copy link
Member

liggitt commented Mar 19, 2022

looking really solid, thanks. noted a few places we want to make sure we exercise plumbing with tests

Copy link
Member

@liggitt liggitt left a comment

Choose a reason for hiding this comment

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

one way to improve coverage of tested code paths is to remove unused ones... I noted several places where we have no use of the ok data loss return indicator... trimming that to only places that are propagating errors from License/ContactInfo/ExternalDocumentation will help clarify where we could actually exercise those branches in tests

pkg/validation/spec/gnostic.go Outdated Show resolved Hide resolved
pkg/validation/spec/gnostic.go Outdated Show resolved Hide resolved
pkg/validation/spec/gnostic.go Outdated Show resolved Hide resolved
pkg/validation/spec/gnostic.go Outdated Show resolved Hide resolved
pkg/validation/spec/gnostic.go Outdated Show resolved Hide resolved
pkg/validation/spec/gnostic.go Outdated Show resolved Hide resolved
pkg/validation/spec/gnostic.go Outdated Show resolved Hide resolved
pkg/validation/spec/gnostic.go Outdated Show resolved Hide resolved
@alexzielenski alexzielenski force-pushed the gnostic_conversion branch 4 times, most recently from ee5c5ea to 53e58cf Compare March 29, 2022 21:35
@alexzielenski
Copy link
Contributor Author

/lgtm
/approve

Copy link
Member

@liggitt liggitt left a comment

Choose a reason for hiding this comment

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

a couple last nits, then lgtm

pkg/validation/spec/gnostic.go Show resolved Hide resolved
pkg/validation/spec/gnostic.go Show resolved Hide resolved
also add benchmark for gnostic conversion with swagger
@liggitt
Copy link
Member

liggitt commented Apr 1, 2022

/lgtm
/approve

@k8s-ci-robot k8s-ci-robot added the lgtm "Looks good to me", indicates that a PR is ready to be merged. label Apr 1, 2022
@apelisse
Copy link
Member

apelisse commented Apr 1, 2022

/approve

Thanks, that's awesome! Can't wait to see the opposite direction 😂

@k8s-ci-robot
Copy link
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: alexzielenski, apelisse, liggitt

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

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. 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.

4 participants