Skip to content

Commit 9380416

Browse files
authored
Merge pull request kubernetes#77554 from sttts/sttts-structural-publishing
apiextensions: publish (only) structural OpenAPI schemas
2 parents 69c90d8 + d01e942 commit 9380416

File tree

10 files changed

+610
-319
lines changed

10 files changed

+610
-319
lines changed

staging/src/k8s.io/apiextensions-apiserver/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ require (
1616
github.com/go-openapi/strfmt v0.17.0
1717
github.com/go-openapi/validate v0.18.0
1818
github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415
19+
github.com/google/go-cmp v0.3.0
1920
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf
2021
github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d
2122
github.com/inconshreveable/mousetrap v1.0.0 // indirect

staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ go_library(
77
"convert.go",
88
"goopenapi.go",
99
"structural.go",
10+
"unfold.go",
1011
"validation.go",
1112
"visitor.go",
1213
"zz_generated.deepcopy.go",
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
Copyright 2019 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package schema
18+
19+
// Unfold expands vendor extensions of a structural schema.
20+
// It mutates the receiver.
21+
func (s *Structural) Unfold() *Structural {
22+
if s == nil {
23+
return nil
24+
}
25+
26+
mapper := Visitor{
27+
Structural: func(s *Structural) bool {
28+
if !s.XIntOrString {
29+
return false
30+
}
31+
32+
skipAnyOf := isIntOrStringAnyOfPattern(s)
33+
skipFirstAllOfAnyOf := isIntOrStringAllOfPattern(s)
34+
if skipAnyOf || skipFirstAllOfAnyOf {
35+
return false
36+
}
37+
38+
if s.AnyOf == nil {
39+
s.AnyOf = []NestedValueValidation{
40+
{ForbiddenGenerics: Generic{Type: "integer"}},
41+
{ForbiddenGenerics: Generic{Type: "string"}},
42+
}
43+
} else {
44+
s.AllOf = append([]NestedValueValidation{
45+
{
46+
ValueValidation: ValueValidation{
47+
AnyOf: []NestedValueValidation{
48+
{ForbiddenGenerics: Generic{Type: "integer"}},
49+
{ForbiddenGenerics: Generic{Type: "string"}},
50+
},
51+
},
52+
},
53+
}, s.AllOf...)
54+
}
55+
56+
return true
57+
},
58+
NestedValueValidation: nil, // x-kubernetes-int-or-string cannot be set in nested value validation
59+
}
60+
mapper.Visit(s)
61+
62+
return s
63+
}

staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/validation.go

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -97,15 +97,8 @@ func validateStructuralInvariants(s *Structural, lvl level, fldPath *field.Path)
9797
// - type: integer
9898
// - type: string
9999
// - ... zero or more
100-
skipAnyOf := false
101-
skipFirstAllOfAnyOf := false
102-
if s.XIntOrString && s.ValueValidation != nil {
103-
if len(s.ValueValidation.AnyOf) == 2 && reflect.DeepEqual(s.ValueValidation.AnyOf, intOrStringAnyOf) {
104-
skipAnyOf = true
105-
} else if len(s.ValueValidation.AllOf) >= 1 && len(s.ValueValidation.AllOf[0].AnyOf) == 2 && reflect.DeepEqual(s.ValueValidation.AllOf[0].AnyOf, intOrStringAnyOf) {
106-
skipFirstAllOfAnyOf = true
107-
}
108-
}
100+
skipAnyOf := isIntOrStringAnyOfPattern(s)
101+
skipFirstAllOfAnyOf := isIntOrStringAllOfPattern(s)
109102

110103
allErrs = append(allErrs, validateValueValidation(s.ValueValidation, skipAnyOf, skipFirstAllOfAnyOf, lvl, fldPath)...)
111104

@@ -157,6 +150,20 @@ func validateStructuralInvariants(s *Structural, lvl level, fldPath *field.Path)
157150
return allErrs
158151
}
159152

153+
func isIntOrStringAnyOfPattern(s *Structural) bool {
154+
if s == nil || s.ValueValidation == nil {
155+
return false
156+
}
157+
return len(s.ValueValidation.AnyOf) == 2 && reflect.DeepEqual(s.ValueValidation.AnyOf, intOrStringAnyOf)
158+
}
159+
160+
func isIntOrStringAllOfPattern(s *Structural) bool {
161+
if s == nil || s.ValueValidation == nil {
162+
return false
163+
}
164+
return len(s.ValueValidation.AllOf) >= 1 && len(s.ValueValidation.AllOf[0].AnyOf) == 2 && reflect.DeepEqual(s.ValueValidation.AllOf[0].AnyOf, intOrStringAnyOf)
165+
}
166+
160167
// validateGeneric checks the generic fields of a structural schema.
161168
func validateGeneric(g *Generic, lvl level, fldPath *field.Path) field.ErrorList {
162169
if g == nil {

staging/src/k8s.io/apiextensions-apiserver/pkg/controller/openapi/BUILD

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ go_library(
1414
deps = [
1515
"//staging/src/k8s.io/api/autoscaling/v1:go_default_library",
1616
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
17-
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation:go_default_library",
17+
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema:go_default_library",
1818
"//staging/src/k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/apiextensions/internalversion:go_default_library",
1919
"//staging/src/k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion:go_default_library",
2020
"//staging/src/k8s.io/apiextensions-apiserver/pkg/generated/openapi:go_default_library",
@@ -49,10 +49,12 @@ go_test(
4949
deps = [
5050
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
5151
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library",
52+
"//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema:go_default_library",
5253
"//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library",
5354
"//staging/src/k8s.io/apimachinery/pkg/util/json:go_default_library",
5455
"//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library",
5556
"//vendor/github.com/go-openapi/spec:go_default_library",
57+
"//vendor/github.com/google/go-cmp/cmp:go_default_library",
5658
"//vendor/github.com/google/gofuzz:go_default_library",
5759
"//vendor/github.com/googleapis/gnostic/OpenAPIv2:go_default_library",
5860
"//vendor/github.com/googleapis/gnostic/compiler:go_default_library",

staging/src/k8s.io/apiextensions-apiserver/pkg/controller/openapi/builder.go

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"github.com/go-openapi/spec"
2727

2828
v1 "k8s.io/api/autoscaling/v1"
29+
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
2930
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3031
metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1"
3132
"k8s.io/apimachinery/pkg/runtime"
@@ -58,22 +59,24 @@ var namer *openapi.DefinitionNamer
5859

5960
// BuildSwagger builds swagger for the given crd in the given version
6061
func BuildSwagger(crd *apiextensions.CustomResourceDefinition, version string) (*spec.Swagger, error) {
61-
var schema *spec.Schema
62+
var schema *structuralschema.Structural
6263
s, err := apiextensions.GetSchemaForVersion(crd, version)
6364
if err != nil {
6465
return nil, err
6566
}
6667
if s != nil && s.OpenAPIV3Schema != nil {
67-
schema, err = ConvertJSONSchemaPropsToOpenAPIv2Schema(s.OpenAPIV3Schema)
68-
if err != nil {
69-
return nil, err
68+
ss, err := structuralschema.NewStructural(s.OpenAPIV3Schema)
69+
if err == nil && len(structuralschema.ValidateStructural(ss, nil)) == 0 {
70+
// skip non-structural schemas
71+
schema = ss.Unfold()
7072
}
7173
}
74+
7275
// TODO(roycaihw): remove the WebService templating below. The following logic
7376
// comes from function registerResourceHandlers() in k8s.io/apiserver.
7477
// Alternatives are either (ideally) refactoring registerResourceHandlers() to
7578
// reuse the code, or faking an APIInstaller for CR to feed to registerResourceHandlers().
76-
b := newBuilder(crd, version, schema)
79+
b := newBuilder(crd, version, schema, true)
7780

7881
// Sample response types for building web service
7982
sample := &CRDCanonicalTypeNamer{
@@ -288,31 +291,35 @@ func (b *builder) buildRoute(root, path, action, verb string, sample interface{}
288291

289292
// buildKubeNative builds input schema with Kubernetes' native object meta, type meta and
290293
// extensions
291-
func (b *builder) buildKubeNative(schema *spec.Schema) *spec.Schema {
294+
func (b *builder) buildKubeNative(schema *structuralschema.Structural, v2 bool) (ret *spec.Schema) {
292295
// only add properties if we have a schema. Otherwise, kubectl would (wrongly) assume additionalProperties=false
293296
// and forbid anything outside of apiVersion, kind and metadata. We have to fix kubectl to stop doing this, e.g. by
294297
// adding additionalProperties=true support to explicitly allow additional fields.
295298
// TODO: fix kubectl to understand additionalProperties=true
296299
if schema == nil {
297-
schema = &spec.Schema{
300+
ret = &spec.Schema{
298301
SchemaProps: spec.SchemaProps{Type: []string{"object"}},
299302
}
300303
// no, we cannot add more properties here, not even TypeMeta/ObjectMeta because kubectl will complain about
301304
// unknown fields for anything else.
302305
} else {
303-
schema.SetProperty("metadata", *spec.RefSchema(objectMetaSchemaRef).
306+
if v2 {
307+
schema = ToStructuralOpenAPIV2(schema)
308+
}
309+
ret = schema.ToGoOpenAPI()
310+
ret.SetProperty("metadata", *spec.RefSchema(objectMetaSchemaRef).
304311
WithDescription(swaggerPartialObjectMetadataDescriptions["metadata"]))
305-
addTypeMetaProperties(schema)
312+
addTypeMetaProperties(ret)
306313
}
307-
schema.AddExtension(endpoints.ROUTE_META_GVK, []interface{}{
314+
ret.AddExtension(endpoints.ROUTE_META_GVK, []interface{}{
308315
map[string]interface{}{
309316
"group": b.group,
310317
"version": b.version,
311318
"kind": b.kind,
312319
},
313320
})
314321

315-
return schema
322+
return ret
316323
}
317324

318325
// getDefinition gets definition for given Kubernetes type. This function is extracted from
@@ -391,7 +398,7 @@ func (b *builder) getOpenAPIConfig() *common.Config {
391398
}
392399
}
393400

394-
func newBuilder(crd *apiextensions.CustomResourceDefinition, version string, schema *spec.Schema) *builder {
401+
func newBuilder(crd *apiextensions.CustomResourceDefinition, version string, schema *structuralschema.Structural, v2 bool) *builder {
395402
b := &builder{
396403
schema: &spec.Schema{
397404
SchemaProps: spec.SchemaProps{Type: []string{"object"}},
@@ -410,7 +417,7 @@ func newBuilder(crd *apiextensions.CustomResourceDefinition, version string, sch
410417
}
411418

412419
// Pre-build schema with Kubernetes native properties
413-
b.schema = b.buildKubeNative(schema)
420+
b.schema = b.buildKubeNative(schema, v2)
414421
b.listSchema = b.buildListSchema()
415422

416423
return b

0 commit comments

Comments
 (0)