diff --git a/go.mod b/go.mod index f3ad90c25..724cd4b80 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,8 @@ require ( github.com/onsi/gomega v1.33.1 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 - golang.org/x/tools v0.20.0 + golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 + golang.org/x/tools v0.22.0 google.golang.org/protobuf v1.33.0 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 @@ -42,9 +43,9 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/mod v0.17.0 // indirect - golang.org/x/net v0.24.0 // indirect + golang.org/x/mod v0.18.0 // indirect + golang.org/x/net v0.26.0 // indirect golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.19.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect ) diff --git a/go.sum b/go.sum index 9f9583403..78b463ecc 100644 --- a/go.sum +++ b/go.sum @@ -69,18 +69,20 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= -golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= diff --git a/pkg/common/common.go b/pkg/common/common.go index e4ce843b0..aedea8826 100644 --- a/pkg/common/common.go +++ b/pkg/common/common.go @@ -18,6 +18,9 @@ package common import ( "net/http" + + "golang.org/x/exp/slices" + "strings" "github.com/emicklei/go-restful/v3" @@ -287,3 +290,15 @@ func GenerateOpenAPIV3OneOfSchema(types []string) (oneOf []spec.Schema) { } return } + +// MaybePopulateIntOrString populates the x-int-or-string extension only if +// the oneOf type refers to supporting both an int (integer/number) and string. +func MaybePopulateIntOrString(oneOf []string, ext spec.Extensions) spec.Extensions { + if len(oneOf) != 2 { + return ext + } + if slices.Contains(oneOf, "string") && (slices.Contains(oneOf, "integer") || slices.Contains(oneOf, "number")) { + ext["x-kubernetes-int-or-string"] = true + } + return ext +} diff --git a/pkg/generators/openapi.go b/pkg/generators/openapi.go index b0fa8272a..d2c4df799 100644 --- a/pkg/generators/openapi.go +++ b/pkg/generators/openapi.go @@ -551,7 +551,7 @@ func (g openAPITypeWriter) generate(t *types.Type) error { return err } g.Do("},\n", nil) - if err := g.generateStructExtensions(t, validationSchema.Extensions); err != nil { + if err := g.generateStructExtensions(t, validationSchema.Extensions, false, args); err != nil { return err } g.Do("},\n", nil) @@ -570,7 +570,7 @@ func (g openAPITypeWriter) generate(t *types.Type) error { return err } g.Do("},\n", nil) - if err := g.generateStructExtensions(t, validationSchema.Extensions); err != nil { + if err := g.generateStructExtensions(t, validationSchema.Extensions, true, args); err != nil { return err } g.Do("},\n", nil) @@ -587,7 +587,7 @@ func (g openAPITypeWriter) generate(t *types.Type) error { return err } g.Do("},\n", nil) - if err := g.generateStructExtensions(t, validationSchema.Extensions); err != nil { + if err := g.generateStructExtensions(t, validationSchema.Extensions, false, args); err != nil { return err } g.Do("},\n", nil) @@ -605,7 +605,7 @@ func (g openAPITypeWriter) generate(t *types.Type) error { return err } g.Do("},\n", nil) - if err := g.generateStructExtensions(t, validationSchema.Extensions); err != nil { + if err := g.generateStructExtensions(t, validationSchema.Extensions, false, args); err != nil { return err } g.Do("},\n", nil) @@ -643,7 +643,7 @@ func (g openAPITypeWriter) generate(t *types.Type) error { g.Do("Required: []string{\"$.$\"},\n", strings.Join(required, "\",\"")) } g.Do("},\n", nil) - if err := g.generateStructExtensions(t, validationSchema.Extensions); err != nil { + if err := g.generateStructExtensions(t, validationSchema.Extensions, false, args); err != nil { return err } g.Do("},\n", nil) @@ -676,7 +676,7 @@ func (g openAPITypeWriter) generate(t *types.Type) error { return nil } -func (g openAPITypeWriter) generateStructExtensions(t *types.Type, otherExtensions map[string]interface{}) error { +func (g openAPITypeWriter) generateStructExtensions(t *types.Type, otherExtensions map[string]interface{}, checkIntOrString bool, args generator.Args) error { extensions, errors := parseExtensions(t.CommentLines) // Initially, we will only log struct extension errors. if len(errors) > 0 { @@ -692,7 +692,7 @@ func (g openAPITypeWriter) generateStructExtensions(t *types.Type, otherExtensio } // TODO(seans3): Validate struct extensions here. - g.emitExtensions(extensions, unions, otherExtensions) + g.emitExtensions(extensions, unions, otherExtensions, checkIntOrString, args) return nil } @@ -707,16 +707,20 @@ func (g openAPITypeWriter) generateMemberExtensions(m *types.Member, parent *typ klog.V(2).Infof("%s %s\n", errorPrefix, e) } } - g.emitExtensions(extensions, nil, otherExtensions) + g.emitExtensions(extensions, nil, otherExtensions, false, generator.Args{}) return nil } -func (g openAPITypeWriter) emitExtensions(extensions []extension, unions []union, otherExtensions map[string]interface{}) { +func (g openAPITypeWriter) emitExtensions(extensions []extension, unions []union, otherExtensions map[string]interface{}, checkIntOrString bool, args generator.Args) { // If any extensions exist, then emit code to create them. - if len(extensions) == 0 && len(unions) == 0 && len(otherExtensions) == 0 { + if len(extensions) == 0 && len(unions) == 0 && len(otherExtensions) == 0 && !checkIntOrString { return } - g.Do("VendorExtensible: spec.VendorExtensible{\nExtensions: spec.Extensions{\n", nil) + if checkIntOrString { + g.Do("VendorExtensible: spec.VendorExtensible{\nExtensions: common.MaybePopulateIntOrString($.type|raw${}.OpenAPIV3OneOfTypes(), spec.Extensions{\n", args) + } else { + g.Do("VendorExtensible: spec.VendorExtensible{\nExtensions: spec.Extensions{\n", nil) + } for _, extension := range extensions { g.Do("\"$.$\": ", extension.xName) if extension.hasMultipleValues() || extension.isAlwaysArrayFormat() { @@ -753,8 +757,12 @@ func (g openAPITypeWriter) emitExtensions(extensions []extension, unions []union }) } } + g.Do("},\n", nil) + if checkIntOrString { + g.Do(")", nil) + } - g.Do("},\n},\n", nil) + g.Do("},\n", nil) } // TODO(#44005): Move this validation outside of this generator (probably to policy verifier) diff --git a/pkg/generators/openapi_test.go b/pkg/generators/openapi_test.go index e53292129..0621d76f4 100644 --- a/pkg/generators/openapi_test.go +++ b/pkg/generators/openapi_test.go @@ -1454,6 +1454,10 @@ Description: "Blah is a custom type", OneOf:common.GenerateOpenAPIV3OneOfSchema(foo.Blah{}.OpenAPIV3OneOfTypes()), Format:foo.Blah{}.OpenAPISchemaFormat(), }, +VendorExtensible: spec.VendorExtensible{ +Extensions: common.MaybePopulateIntOrString(foo.Blah{}.OpenAPIV3OneOfTypes(), spec.Extensions{ +}, +)}, }, },common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -2913,6 +2917,10 @@ OneOf:common.GenerateOpenAPIV3OneOfSchema(foo.Blah{}.OpenAPIV3OneOfTypes()), Format:foo.Blah{}.OpenAPISchemaFormat(), MaxLength: ptr.To[int64](10), }, +VendorExtensible: spec.VendorExtensible{ +Extensions: common.MaybePopulateIntOrString(foo.Blah{}.OpenAPIV3OneOfTypes(), spec.Extensions{ +}, +)}, }, },common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/test/integration/go.mod b/test/integration/go.mod index 915ea8dd4..2b61698c6 100644 --- a/test/integration/go.mod +++ b/test/integration/go.mod @@ -29,12 +29,13 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/spf13/pflag v1.0.5 // indirect - golang.org/x/mod v0.17.0 // indirect - golang.org/x/net v0.24.0 // indirect + golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect + golang.org/x/mod v0.18.0 // indirect + golang.org/x/net v0.26.0 // indirect golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.19.0 // indirect - golang.org/x/text v0.14.0 // indirect - golang.org/x/tools v0.20.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + golang.org/x/tools v0.22.0 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/test/integration/go.sum b/test/integration/go.sum index c276eb04d..15ea208fe 100644 --- a/test/integration/go.sum +++ b/test/integration/go.sum @@ -71,18 +71,20 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= -golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= diff --git a/test/integration/pkg/generated/openapi_generated.go b/test/integration/pkg/generated/openapi_generated.go index 993ef5815..4c14a57a7 100644 --- a/test/integration/pkg/generated/openapi_generated.go +++ b/test/integration/pkg/generated/openapi_generated.go @@ -89,6 +89,8 @@ func schema_test_integration_testdata_custom_FooV3OneOf(ref common.ReferenceCall OneOf: common.GenerateOpenAPIV3OneOfSchema(custom.FooV3OneOf{}.OpenAPIV3OneOfTypes()), Format: custom.FooV3OneOf{}.OpenAPISchemaFormat(), }, + VendorExtensible: spec.VendorExtensible{ + Extensions: common.MaybePopulateIntOrString(custom.FooV3OneOf{}.OpenAPIV3OneOfTypes(), spec.Extensions{})}, }, }, common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -1127,10 +1129,10 @@ func schema_test_integration_testdata_valuevalidation_Foo3(ref common.ReferenceC MaxProperties: ptr.To[int64](5), }, VendorExtensible: spec.VendorExtensible{ - Extensions: spec.Extensions{ + Extensions: common.MaybePopulateIntOrString(valuevalidation.Foo3{}.OpenAPIV3OneOfTypes(), spec.Extensions{ "x-kubernetes-validations": []interface{}{map[string]interface{}{"message": "foo3", "rule": "self == oldSelf"}}, }, - }, + )}, }, }, common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/test/integration/testdata/golden.v3.json b/test/integration/testdata/golden.v3.json index 7a3b832a8..e0566f757 100644 --- a/test/integration/testdata/golden.v3.json +++ b/test/integration/testdata/golden.v3.json @@ -1265,6 +1265,7 @@ "type": "string" } ], + "x-kubernetes-int-or-string": true, "x-kubernetes-validations": [ { "message": "foo3",