diff --git a/api/api-rules/violation_exceptions.list b/api/api-rules/violation_exceptions.list index 4fae72c7a94..70e1fa40f6f 100644 --- a/api/api-rules/violation_exceptions.list +++ b/api/api-rules/violation_exceptions.list @@ -30,6 +30,9 @@ API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiexten API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1,JSON,Raw API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1,JSONSchemaProps,Ref API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1,JSONSchemaProps,Schema +API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1,JSONSchemaProps,XEmbeddedResource +API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1,JSONSchemaProps,XIntOrString +API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1,JSONSchemaProps,XPreserveUnknownFields API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1,JSONSchemaPropsOrArray,JSONSchemas API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1,JSONSchemaPropsOrArray,Schema API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1,JSONSchemaPropsOrBool,Allows diff --git a/api/openapi-spec/swagger.json b/api/openapi-spec/swagger.json index 5fad24c09f2..e5a88859755 100644 --- a/api/openapi-spec/swagger.json +++ b/api/openapi-spec/swagger.json @@ -16808,6 +16808,18 @@ }, "uniqueItems": { "type": "boolean" + }, + "x-kubernetes-embedded-resource": { + "description": "x-kubernetes-embedded-resource defines that the value is an embedded Kubernetes runtime.Object, with TypeMeta and ObjectMeta. The type must be object. It is allowed to further restrict the embedded object. kind, apiVersion and metadata are validated automatically. x-kubernetes-preserve-unknown-fields is allowed to be true, but does not have to be if the object is fully specified (up to kind, apiVersion, metadata).", + "type": "boolean" + }, + "x-kubernetes-int-or-string": { + "description": "x-kubernetes-int-or-string specifies that this value is either an integer or a string. If this is true, an empty type is allowed and type as child of anyOf is permitted if following one of the following patterns:\n\n1) anyOf:\n - type: integer\n - type: string\n2) allOf:\n - anyOf:\n - type: integer\n - type: string\n - ... zero or more", + "type": "boolean" + }, + "x-kubernetes-preserve-unknown-fields": { + "description": "x-kubernetes-preserve-unknown-fields stops the API server decoding step from pruning fields which are not specified in the validation schema. This affects fields recursively, but switches back to normal pruning behaviour if nested properties or additionalProperties are specified in the schema.", + "type": "boolean" } }, "type": "object" diff --git a/staging/src/k8s.io/apiextensions-apiserver/BUILD b/staging/src/k8s.io/apiextensions-apiserver/BUILD index a52a7a2b2f0..946698df4d5 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/BUILD +++ b/staging/src/k8s.io/apiextensions-apiserver/BUILD @@ -47,6 +47,7 @@ filegroup( "//staging/src/k8s.io/apiextensions-apiserver/pkg/cmd/server:all-srcs", "//staging/src/k8s.io/apiextensions-apiserver/pkg/controller/establish:all-srcs", "//staging/src/k8s.io/apiextensions-apiserver/pkg/controller/finalizer:all-srcs", + "//staging/src/k8s.io/apiextensions-apiserver/pkg/controller/nonstructuralschema:all-srcs", "//staging/src/k8s.io/apiextensions-apiserver/pkg/controller/openapi:all-srcs", "//staging/src/k8s.io/apiextensions-apiserver/pkg/controller/status:all-srcs", "//staging/src/k8s.io/apiextensions-apiserver/pkg/crdserverscheme:all-srcs", diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/types.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/types.go index 3af24046bf2..ce6aae71182 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/types.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/types.go @@ -264,6 +264,22 @@ const ( // NamesAccepted means the names chosen for this CustomResourceDefinition do not conflict with others in // the group and are therefore accepted. NamesAccepted CustomResourceDefinitionConditionType = "NamesAccepted" + // NonStructuralSchema means that one or more OpenAPI schema is not structural. + // + // A schema is structural if it specifies types for all values, with the only exceptions of those with + // - x-kubernetes-int-or-string: true — for fields which can be integer or string + // - x-kubernetes-preserve-unknown-fields: true — for raw, unspecified JSON values + // and there is no type, additionalProperties, default, nullable or x-kubernetes-* vendor extenions + // specified under allOf, anyOf, oneOf or not. + // + // Non-structural schemas will not be allowed anymore in v1 API groups. Moreover, new features will not be + // available for non-structural CRDs: + // - pruning + // - defaulting + // - read-only + // - OpenAPI publishing + // - webhook conversion + NonStructuralSchema CustomResourceDefinitionConditionType = "NonStructuralSchema" // Terminating means that the CustomResourceDefinition has been deleted and is cleaning up. Terminating CustomResourceDefinitionConditionType = "Terminating" ) diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/types_jsonschema.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/types_jsonschema.go index af78c34fb6e..e0cba964731 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/types_jsonschema.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/types_jsonschema.go @@ -55,6 +55,36 @@ type JSONSchemaProps struct { Definitions JSONSchemaDefinitions ExternalDocs *ExternalDocumentation Example *JSON + + // x-kubernetes-preserve-unknown-fields stops the API server + // decoding step from pruning fields which are not specified + // in the validation schema. This affects fields recursively, + // but switches back to normal pruning behaviour if nested + // properties or additionalProperties are specified in the schema. + XPreserveUnknownFields bool + + // x-kubernetes-embedded-resource defines that the value is an + // embedded Kubernetes runtime.Object, with TypeMeta and + // ObjectMeta. The type must be object. It is allowed to further + // restrict the embedded object. Both ObjectMeta and TypeMeta + // are validated automatically. x-kubernetes-preserve-unknown-fields + // must be true. + XEmbeddedResource bool + + // x-kubernetes-int-or-string specifies that this value is + // either an integer or a string. If this is true, an empty + // type is allowed and type as child of anyOf is permitted + // if following one of the following patterns: + // + // 1) anyOf: + // - type: integer + // - type: string + // 2) allOf: + // - anyOf: + // - type: integer + // - type: string + // - ... zero or more + XIntOrString bool } // JSON represents any valid JSON value. diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/generated.pb.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/generated.pb.go index 58cc3b64c05..c928a97d2a6 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/generated.pb.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/generated.pb.go @@ -1431,6 +1431,36 @@ func (m *JSONSchemaProps) MarshalTo(dAtA []byte) (int, error) { dAtA[i] = 0 } i++ + dAtA[i] = 0xb0 + i++ + dAtA[i] = 0x2 + i++ + if m.XPreserveUnknownFields { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i++ + dAtA[i] = 0xb8 + i++ + dAtA[i] = 0x2 + i++ + if m.XEmbeddedResource { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i++ + dAtA[i] = 0xc0 + i++ + dAtA[i] = 0x2 + i++ + if m.XIntOrString { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i++ return i, nil } @@ -2080,6 +2110,9 @@ func (m *JSONSchemaProps) Size() (n int) { n += 2 + l + sovGenerated(uint64(l)) } n += 3 + n += 3 + n += 3 + n += 3 return n } @@ -2478,6 +2511,9 @@ func (this *JSONSchemaProps) String() string { `ExternalDocs:` + strings.Replace(fmt.Sprintf("%v", this.ExternalDocs), "ExternalDocumentation", "ExternalDocumentation", 1) + `,`, `Example:` + strings.Replace(fmt.Sprintf("%v", this.Example), "JSON", "JSON", 1) + `,`, `Nullable:` + fmt.Sprintf("%v", this.Nullable) + `,`, + `XPreserveUnknownFields:` + fmt.Sprintf("%v", this.XPreserveUnknownFields) + `,`, + `XEmbeddedResource:` + fmt.Sprintf("%v", this.XEmbeddedResource) + `,`, + `XIntOrString:` + fmt.Sprintf("%v", this.XIntOrString) + `,`, `}`, }, "") return s @@ -6603,6 +6639,66 @@ func (m *JSONSchemaProps) Unmarshal(dAtA []byte) error { } } m.Nullable = bool(v != 0) + case 38: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field XPreserveUnknownFields", wireType) + } + var v int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + m.XPreserveUnknownFields = bool(v != 0) + case 39: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field XEmbeddedResource", wireType) + } + var v int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + m.XEmbeddedResource = bool(v != 0) + case 40: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field XIntOrString", wireType) + } + var v int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + m.XIntOrString = bool(v != 0) default: iNdEx = preIndex skippy, err := skipGenerated(dAtA[iNdEx:]) @@ -7365,180 +7461,186 @@ func init() { } var fileDescriptorGenerated = []byte{ - // 2796 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xcc, 0x5a, 0x4b, 0x6f, 0x23, 0xc7, - 0xf1, 0xdf, 0x21, 0x45, 0x89, 0x6a, 0x49, 0x2b, 0xa9, 0xd7, 0x5a, 0xcf, 0xea, 0x2f, 0x93, 0x12, - 0xfd, 0xb7, 0xa1, 0x38, 0xbb, 0x94, 0xbd, 0xb1, 0x63, 0xc7, 0x40, 0x0e, 0x22, 0x25, 0x1b, 0x72, - 0x56, 0x8f, 0x34, 0x77, 0x6d, 0x27, 0x7e, 0xb6, 0x86, 0x4d, 0x6a, 0x56, 0xf3, 0xda, 0xe9, 0x19, - 0x4a, 0x82, 0x93, 0x20, 0x0f, 0x18, 0x09, 0x82, 0x24, 0x0e, 0xe2, 0xbd, 0x04, 0x49, 0x0e, 0x4e, - 0x90, 0x4b, 0x0e, 0xc9, 0x21, 0xb9, 0x25, 0x1f, 0x60, 0x8f, 0x46, 0x4e, 0x46, 0x10, 0x10, 0x59, - 0xfa, 0x2b, 0x04, 0x08, 0xa0, 0x53, 0xd0, 0x8f, 0xe9, 0x19, 0x0e, 0xc9, 0x5d, 0xc1, 0x4b, 0x7a, - 0x73, 0xd3, 0x54, 0x55, 0xd7, 0xaf, 0xba, 0xba, 0xaa, 0xba, 0xba, 0x28, 0xd0, 0x38, 0x7c, 0x81, - 0x96, 0x4d, 0x77, 0xed, 0x30, 0xdc, 0x27, 0xbe, 0x43, 0x02, 0x42, 0xd7, 0x5a, 0xc4, 0xa9, 0xbb, - 0xfe, 0x9a, 0x64, 0x60, 0xcf, 0x24, 0xc7, 0x01, 0x71, 0xa8, 0xe9, 0x3a, 0xf4, 0x0a, 0xf6, 0x4c, - 0x4a, 0xfc, 0x16, 0xf1, 0xd7, 0xbc, 0xc3, 0x26, 0xe3, 0xd1, 0x6e, 0x81, 0xb5, 0xd6, 0x33, 0xfb, - 0x24, 0xc0, 0xcf, 0xac, 0x35, 0x89, 0x43, 0x7c, 0x1c, 0x90, 0x7a, 0xd9, 0xf3, 0xdd, 0xc0, 0x85, - 0x5f, 0x15, 0xea, 0xca, 0x5d, 0xd2, 0xef, 0x28, 0x75, 0x65, 0xef, 0xb0, 0xc9, 0x78, 0xb4, 0x5b, - 0xa0, 0x2c, 0xd5, 0x2d, 0x5e, 0x69, 0x9a, 0xc1, 0x41, 0xb8, 0x5f, 0x36, 0x5c, 0x7b, 0xad, 0xe9, - 0x36, 0xdd, 0x35, 0xae, 0x75, 0x3f, 0x6c, 0xf0, 0x2f, 0xfe, 0xc1, 0xff, 0x12, 0x68, 0x8b, 0xcf, - 0xc6, 0xc6, 0xdb, 0xd8, 0x38, 0x30, 0x1d, 0xe2, 0x9f, 0xc4, 0x16, 0xdb, 0x24, 0xc0, 0x6b, 0xad, - 0x1e, 0x1b, 0x17, 0xd7, 0x06, 0xad, 0xf2, 0x43, 0x27, 0x30, 0x6d, 0xd2, 0xb3, 0xe0, 0xcb, 0xf7, - 0x5b, 0x40, 0x8d, 0x03, 0x62, 0xe3, 0xf4, 0xba, 0xd2, 0xa9, 0x06, 0xe6, 0xab, 0xae, 0xd3, 0x22, - 0x3e, 0xdb, 0x25, 0x22, 0xb7, 0x42, 0x42, 0x03, 0x58, 0x01, 0xd9, 0xd0, 0xac, 0xeb, 0xda, 0xb2, - 0xb6, 0x3a, 0x59, 0x79, 0xfa, 0x4e, 0xbb, 0x78, 0xae, 0xd3, 0x2e, 0x66, 0x6f, 0x6c, 0x6d, 0x9c, - 0xb6, 0x8b, 0x2b, 0x83, 0x90, 0x82, 0x13, 0x8f, 0xd0, 0xf2, 0x8d, 0xad, 0x0d, 0xc4, 0x16, 0xc3, - 0x97, 0xc1, 0x7c, 0x9d, 0x50, 0xd3, 0x27, 0xf5, 0xf5, 0xbd, 0xad, 0x57, 0x85, 0x7e, 0x3d, 0xc3, - 0x35, 0x5e, 0x92, 0x1a, 0xe7, 0x37, 0xd2, 0x02, 0xa8, 0x77, 0x0d, 0x7c, 0x1d, 0x4c, 0xb8, 0xfb, - 0x37, 0x89, 0x11, 0x50, 0x3d, 0xbb, 0x9c, 0x5d, 0x9d, 0xba, 0x7a, 0xa5, 0x1c, 0x9f, 0xa0, 0x32, - 0x81, 0x1f, 0x9b, 0xdc, 0x6c, 0x19, 0xe1, 0xa3, 0xcd, 0xe8, 0xe4, 0x2a, 0xb3, 0x12, 0x6d, 0x62, - 0x57, 0x68, 0x41, 0x91, 0xba, 0xd2, 0xef, 0x32, 0x00, 0x26, 0x37, 0x4f, 0x3d, 0xd7, 0xa1, 0x64, - 0x28, 0xbb, 0xa7, 0x60, 0xce, 0xe0, 0x9a, 0x03, 0x52, 0x97, 0xb8, 0x7a, 0xe6, 0xb3, 0x58, 0xaf, - 0x4b, 0xfc, 0xb9, 0x6a, 0x4a, 0x1d, 0xea, 0x01, 0x80, 0xd7, 0xc1, 0xb8, 0x4f, 0x68, 0x68, 0x05, - 0x7a, 0x76, 0x59, 0x5b, 0x9d, 0xba, 0x7a, 0x79, 0x20, 0x14, 0x8f, 0x6f, 0x16, 0x7c, 0xe5, 0xd6, - 0x33, 0xe5, 0x5a, 0x80, 0x83, 0x90, 0x56, 0xce, 0x4b, 0xa4, 0x71, 0xc4, 0x75, 0x20, 0xa9, 0xab, - 0xf4, 0xa3, 0x0c, 0x98, 0x4b, 0x7a, 0xa9, 0x65, 0x92, 0x23, 0x78, 0x04, 0x26, 0x7c, 0x11, 0x2c, - 0xdc, 0x4f, 0x53, 0x57, 0xf7, 0xca, 0x0f, 0x94, 0x56, 0xe5, 0x9e, 0x20, 0xac, 0x4c, 0xb1, 0x33, - 0x93, 0x1f, 0x28, 0x42, 0x83, 0xef, 0x81, 0xbc, 0x2f, 0x0f, 0x8a, 0x47, 0xd3, 0xd4, 0xd5, 0xaf, - 0x0f, 0x11, 0x59, 0x28, 0xae, 0x4c, 0x77, 0xda, 0xc5, 0x7c, 0xf4, 0x85, 0x14, 0x60, 0xe9, 0xc3, - 0x0c, 0x28, 0x54, 0x43, 0x1a, 0xb8, 0x36, 0x22, 0xd4, 0x0d, 0x7d, 0x83, 0x54, 0x5d, 0x2b, 0xb4, - 0x9d, 0x0d, 0xd2, 0x30, 0x1d, 0x33, 0x60, 0xd1, 0xba, 0x0c, 0xc6, 0x1c, 0x6c, 0x13, 0x19, 0x3d, - 0xd3, 0xd2, 0xa7, 0x63, 0x3b, 0xd8, 0x26, 0x88, 0x73, 0x98, 0x04, 0x0b, 0x16, 0x99, 0x0b, 0x4a, - 0xe2, 0xfa, 0x89, 0x47, 0x10, 0xe7, 0xc0, 0x27, 0xc1, 0x78, 0xc3, 0xf5, 0x6d, 0x2c, 0xce, 0x71, - 0x32, 0x3e, 0x99, 0x97, 0x38, 0x15, 0x49, 0x2e, 0x7c, 0x0e, 0x4c, 0xd5, 0x09, 0x35, 0x7c, 0xd3, - 0x63, 0xd0, 0xfa, 0x18, 0x17, 0xbe, 0x20, 0x85, 0xa7, 0x36, 0x62, 0x16, 0x4a, 0xca, 0xc1, 0xcb, - 0x20, 0xef, 0xf9, 0xa6, 0xeb, 0x9b, 0xc1, 0x89, 0x9e, 0x5b, 0xd6, 0x56, 0x73, 0x95, 0x39, 0xb9, - 0x26, 0xbf, 0x27, 0xe9, 0x48, 0x49, 0xc0, 0x65, 0x90, 0x7f, 0xa5, 0xb6, 0xbb, 0xb3, 0x87, 0x83, - 0x03, 0x7d, 0x9c, 0x23, 0x8c, 0x31, 0x69, 0x94, 0xbf, 0x29, 0xa9, 0xa5, 0x7f, 0x66, 0x80, 0x9e, - 0xf6, 0x4a, 0xe4, 0x52, 0xf8, 0x12, 0xc8, 0xd3, 0x80, 0x55, 0x9c, 0xe6, 0x89, 0xf4, 0xc9, 0x53, - 0x11, 0x58, 0x4d, 0xd2, 0x4f, 0xdb, 0xc5, 0x8b, 0xf1, 0x8a, 0x88, 0xca, 0xfd, 0xa1, 0xd6, 0xc2, - 0xdf, 0x68, 0xe0, 0xc2, 0x11, 0xd9, 0x3f, 0x70, 0xdd, 0xc3, 0xaa, 0x65, 0x12, 0x27, 0xa8, 0xba, - 0x4e, 0xc3, 0x6c, 0xca, 0x18, 0x40, 0x0f, 0x18, 0x03, 0xaf, 0xf5, 0x6a, 0xae, 0x3c, 0xda, 0x69, - 0x17, 0x2f, 0xf4, 0x61, 0xa0, 0x7e, 0x76, 0xc0, 0xd7, 0x81, 0x6e, 0xa4, 0x92, 0x44, 0x16, 0x30, - 0x51, 0xb6, 0x26, 0x2b, 0x4b, 0x9d, 0x76, 0x51, 0xaf, 0x0e, 0x90, 0x41, 0x03, 0x57, 0x97, 0x7e, - 0x90, 0x4d, 0xbb, 0x37, 0x11, 0x6e, 0xef, 0x82, 0x3c, 0x4b, 0xe3, 0x3a, 0x0e, 0xb0, 0x4c, 0xc4, - 0xa7, 0xcf, 0x96, 0xf4, 0xa2, 0x66, 0x6c, 0x93, 0x00, 0x57, 0xa0, 0x3c, 0x10, 0x10, 0xd3, 0x90, - 0xd2, 0x0a, 0xbf, 0x0d, 0xc6, 0xa8, 0x47, 0x0c, 0xe9, 0xe8, 0x37, 0x1e, 0x34, 0xd9, 0x06, 0x6c, - 0xa4, 0xe6, 0x11, 0x23, 0xce, 0x05, 0xf6, 0x85, 0x38, 0x2c, 0x7c, 0x5f, 0x03, 0xe3, 0x94, 0x17, - 0x28, 0x59, 0xd4, 0xde, 0x1a, 0x95, 0x05, 0xa9, 0x2a, 0x28, 0xbe, 0x91, 0x04, 0x2f, 0xfd, 0x3b, - 0x03, 0x56, 0x06, 0x2d, 0xad, 0xba, 0x4e, 0x5d, 0x1c, 0xc7, 0x96, 0xcc, 0x6d, 0x11, 0xe9, 0xcf, - 0x25, 0x73, 0xfb, 0xb4, 0x5d, 0x7c, 0xe2, 0xbe, 0x0a, 0x12, 0x45, 0xe0, 0x2b, 0x6a, 0xdf, 0xa2, - 0x50, 0xac, 0x74, 0x1b, 0x76, 0xda, 0x2e, 0xce, 0xaa, 0x65, 0xdd, 0xb6, 0xc2, 0x16, 0x80, 0x16, - 0xa6, 0xc1, 0x75, 0x1f, 0x3b, 0x54, 0xa8, 0x35, 0x6d, 0x22, 0xdd, 0xf7, 0xd4, 0xd9, 0xc2, 0x83, - 0xad, 0xa8, 0x2c, 0x4a, 0x48, 0x78, 0xad, 0x47, 0x1b, 0xea, 0x83, 0xc0, 0xea, 0x96, 0x4f, 0x30, - 0x55, 0xa5, 0x28, 0x71, 0xa3, 0x30, 0x2a, 0x92, 0x5c, 0xf8, 0x05, 0x30, 0x61, 0x13, 0x4a, 0x71, - 0x93, 0xf0, 0xfa, 0x33, 0x19, 0x5f, 0xd1, 0xdb, 0x82, 0x8c, 0x22, 0x3e, 0xeb, 0x4f, 0x96, 0x06, - 0x79, 0xed, 0x9a, 0x49, 0x03, 0xf8, 0x66, 0x4f, 0x02, 0x94, 0xcf, 0xb6, 0x43, 0xb6, 0x9a, 0x87, - 0xbf, 0x2a, 0x7e, 0x11, 0x25, 0x11, 0xfc, 0xdf, 0x02, 0x39, 0x33, 0x20, 0x76, 0x74, 0x77, 0xbf, - 0x36, 0xa2, 0xd8, 0xab, 0xcc, 0x48, 0x1b, 0x72, 0x5b, 0x0c, 0x0d, 0x09, 0xd0, 0xd2, 0xef, 0x33, - 0xe0, 0xb1, 0x41, 0x4b, 0xd8, 0x85, 0x42, 0x99, 0xc7, 0x3d, 0x2b, 0xf4, 0xb1, 0x25, 0x23, 0x4e, - 0x79, 0x7c, 0x8f, 0x53, 0x91, 0xe4, 0xb2, 0x92, 0x4f, 0x4d, 0xa7, 0x19, 0x5a, 0xd8, 0x97, 0xe1, - 0xa4, 0x76, 0x5d, 0x93, 0x74, 0xa4, 0x24, 0x60, 0x19, 0x00, 0x7a, 0xe0, 0xfa, 0x01, 0xc7, 0x90, - 0xd5, 0xeb, 0x3c, 0x2b, 0x10, 0x35, 0x45, 0x45, 0x09, 0x09, 0x76, 0xa3, 0x1d, 0x9a, 0x4e, 0x5d, - 0x9e, 0xba, 0xca, 0xe2, 0xaf, 0x99, 0x4e, 0x1d, 0x71, 0x0e, 0xc3, 0xb7, 0x4c, 0x1a, 0x30, 0x8a, - 0x3c, 0xf2, 0x2e, 0xaf, 0x73, 0x49, 0x25, 0xc1, 0xf0, 0x0d, 0x56, 0xf5, 0x5d, 0xdf, 0x24, 0x54, - 0x1f, 0x8f, 0xf1, 0xab, 0x8a, 0x8a, 0x12, 0x12, 0xa5, 0x5f, 0xe5, 0x07, 0x07, 0x09, 0x2b, 0x25, - 0xf0, 0x71, 0x90, 0x6b, 0xfa, 0x6e, 0xe8, 0x49, 0x2f, 0x29, 0x6f, 0xbf, 0xcc, 0x88, 0x48, 0xf0, - 0x58, 0x54, 0xb6, 0xba, 0xda, 0x54, 0x15, 0x95, 0x51, 0x73, 0x1a, 0xf1, 0xe1, 0xf7, 0x34, 0x90, - 0x73, 0xa4, 0x73, 0x58, 0xc8, 0xbd, 0x39, 0xa2, 0xb8, 0xe0, 0xee, 0x8d, 0xcd, 0x15, 0x9e, 0x17, - 0xc8, 0xf0, 0x59, 0x90, 0xa3, 0x86, 0xeb, 0x11, 0xe9, 0xf5, 0x42, 0x24, 0x54, 0x63, 0xc4, 0xd3, - 0x76, 0x71, 0x26, 0x52, 0xc7, 0x09, 0x48, 0x08, 0xc3, 0x1f, 0x6a, 0x00, 0xb4, 0xb0, 0x65, 0xd6, - 0x31, 0x6f, 0x19, 0x72, 0xdc, 0xfc, 0xe1, 0x86, 0xf5, 0xab, 0x4a, 0xbd, 0x38, 0xb4, 0xf8, 0x1b, - 0x25, 0xa0, 0xe1, 0x07, 0x1a, 0x98, 0xa6, 0xe1, 0xbe, 0x2f, 0x57, 0x51, 0xde, 0x5c, 0x4c, 0x5d, - 0xfd, 0xc6, 0x50, 0x6d, 0xa9, 0x25, 0x00, 0x2a, 0x73, 0x9d, 0x76, 0x71, 0x3a, 0x49, 0x41, 0x5d, - 0x06, 0xc0, 0x9f, 0x68, 0x20, 0xdf, 0x8a, 0xee, 0xec, 0x09, 0x9e, 0xf0, 0x6f, 0x8f, 0xe8, 0x60, - 0x65, 0x44, 0xc5, 0x59, 0xa0, 0xfa, 0x00, 0x65, 0x01, 0xfc, 0xab, 0x06, 0x74, 0x5c, 0x17, 0x05, - 0x1e, 0x5b, 0x7b, 0xbe, 0xe9, 0x04, 0xc4, 0x17, 0xfd, 0x26, 0xd5, 0xf3, 0xdc, 0xbc, 0xe1, 0xde, - 0x85, 0xe9, 0x5e, 0xb6, 0xb2, 0x2c, 0xad, 0xd3, 0xd7, 0x07, 0x98, 0x81, 0x06, 0x1a, 0xc8, 0x03, - 0x2d, 0x6e, 0x69, 0xf4, 0xc9, 0x11, 0x04, 0x5a, 0xdc, 0x4b, 0xc9, 0xea, 0x10, 0x77, 0x50, 0x09, - 0xe8, 0xd2, 0x07, 0xd9, 0x74, 0xd3, 0x9e, 0xbe, 0xf4, 0xe1, 0x6d, 0x61, 0xac, 0xd8, 0x0a, 0xd5, - 0x35, 0xee, 0xdc, 0x77, 0x47, 0x74, 0xf6, 0xea, 0xd6, 0x8e, 0x1b, 0x2f, 0x45, 0xa2, 0x28, 0x61, - 0x07, 0xfc, 0xa5, 0x06, 0x66, 0xb0, 0x61, 0x10, 0x2f, 0x20, 0x75, 0x51, 0x8b, 0x33, 0x9f, 0x43, - 0xb9, 0x59, 0x90, 0x56, 0xcd, 0xac, 0x27, 0xa1, 0x51, 0xb7, 0x25, 0xf0, 0x45, 0x70, 0x9e, 0x06, - 0xae, 0x4f, 0xea, 0xa9, 0x2e, 0x17, 0x76, 0xda, 0xc5, 0xf3, 0xb5, 0x2e, 0x0e, 0x4a, 0x49, 0x96, - 0x3e, 0x1d, 0x03, 0xc5, 0xfb, 0x64, 0xc6, 0x19, 0xde, 0x51, 0x4f, 0x82, 0x71, 0xbe, 0xdd, 0x3a, - 0xf7, 0x4a, 0x3e, 0xd1, 0xb9, 0x71, 0x2a, 0x92, 0x5c, 0x56, 0xd7, 0x19, 0x3e, 0xeb, 0x36, 0xb2, - 0x5c, 0x50, 0xd5, 0xf5, 0x9a, 0x20, 0xa3, 0x88, 0x0f, 0xdf, 0x03, 0xe3, 0x62, 0x4e, 0xc2, 0x8b, - 0xea, 0x08, 0x0b, 0x23, 0xe0, 0x76, 0x72, 0x28, 0x24, 0x21, 0x7b, 0x0b, 0x62, 0xee, 0x61, 0x17, - 0xc4, 0x7b, 0x56, 0xa0, 0xf1, 0xff, 0xf1, 0x0a, 0x54, 0xfa, 0x8f, 0x96, 0xce, 0xfb, 0xc4, 0x56, - 0x6b, 0x06, 0xb6, 0x08, 0xdc, 0x00, 0x73, 0xec, 0x91, 0x81, 0x88, 0x67, 0x99, 0x06, 0xa6, 0xfc, - 0x8d, 0x2b, 0x02, 0x4e, 0x8d, 0x5d, 0x6a, 0x29, 0x3e, 0xea, 0x59, 0x01, 0x5f, 0x01, 0x50, 0x34, - 0xde, 0x5d, 0x7a, 0x44, 0x0f, 0xa1, 0x5a, 0xe8, 0x5a, 0x8f, 0x04, 0xea, 0xb3, 0x0a, 0x56, 0xc1, - 0xbc, 0x85, 0xf7, 0x89, 0x55, 0x23, 0x16, 0x31, 0x02, 0xd7, 0xe7, 0xaa, 0xc4, 0x14, 0x60, 0xa1, - 0xd3, 0x2e, 0xce, 0x5f, 0x4b, 0x33, 0x51, 0xaf, 0x7c, 0x69, 0x25, 0x9d, 0x5e, 0xc9, 0x8d, 0x8b, - 0xe7, 0xcc, 0x47, 0x19, 0xb0, 0x38, 0x38, 0x32, 0xe0, 0xf7, 0xe3, 0x57, 0x97, 0x68, 0xaa, 0xdf, - 0x1e, 0x55, 0x14, 0xca, 0x67, 0x17, 0xe8, 0x7d, 0x72, 0xc1, 0xef, 0xb0, 0x0e, 0x07, 0x5b, 0xd1, - 0x9c, 0xe7, 0xad, 0x91, 0x99, 0xc0, 0x40, 0x2a, 0x93, 0xa2, 0x79, 0xc2, 0x16, 0xef, 0x95, 0xb0, - 0x45, 0x4a, 0x7f, 0xd0, 0xd2, 0x0f, 0xef, 0x38, 0x83, 0xe1, 0x4f, 0x35, 0x30, 0xeb, 0x7a, 0xc4, - 0x59, 0xdf, 0xdb, 0x7a, 0xf5, 0x4b, 0x22, 0x93, 0xa5, 0xab, 0x76, 0x1e, 0xd0, 0xce, 0x57, 0x6a, - 0xbb, 0x3b, 0x42, 0xe1, 0x9e, 0xef, 0x7a, 0xb4, 0x72, 0xa1, 0xd3, 0x2e, 0xce, 0xee, 0x76, 0x43, - 0xa1, 0x34, 0x76, 0xc9, 0x06, 0x0b, 0x9b, 0xc7, 0x01, 0xf1, 0x1d, 0x6c, 0x6d, 0xb8, 0x46, 0x68, - 0x13, 0x27, 0x10, 0x86, 0xa6, 0x86, 0x44, 0xda, 0x19, 0x87, 0x44, 0x8f, 0x81, 0x6c, 0xe8, 0x5b, - 0x32, 0x8a, 0xa7, 0xd4, 0x10, 0x14, 0x5d, 0x43, 0x8c, 0x5e, 0x5a, 0x01, 0x63, 0xcc, 0x4e, 0x78, - 0x09, 0x64, 0x7d, 0x7c, 0xc4, 0xb5, 0x4e, 0x57, 0x26, 0x98, 0x08, 0xc2, 0x47, 0x88, 0xd1, 0x4a, - 0xff, 0x58, 0x02, 0xb3, 0xa9, 0xbd, 0xc0, 0x45, 0x90, 0x51, 0x93, 0x55, 0x20, 0x95, 0x66, 0xb6, - 0x36, 0x50, 0xc6, 0xac, 0xc3, 0xe7, 0x55, 0xf1, 0x15, 0xa0, 0x45, 0x55, 0xcf, 0x39, 0x95, 0xb5, - 0xb4, 0xb1, 0x3a, 0x66, 0x48, 0x54, 0x38, 0x99, 0x0d, 0xa4, 0x21, 0xb3, 0x44, 0xd8, 0x40, 0x1a, - 0x88, 0xd1, 0x3e, 0xeb, 0x84, 0x2c, 0x1a, 0xd1, 0xe5, 0xce, 0x30, 0xa2, 0x1b, 0xbf, 0xe7, 0x88, - 0xee, 0x71, 0x90, 0x0b, 0xcc, 0xc0, 0x22, 0xfa, 0x44, 0xf7, 0xcb, 0xe3, 0x3a, 0x23, 0x22, 0xc1, - 0x83, 0x37, 0xc1, 0x44, 0x9d, 0x34, 0x70, 0x68, 0x05, 0x7a, 0x9e, 0x87, 0x50, 0x75, 0x08, 0x21, - 0x24, 0xe6, 0xa7, 0x1b, 0x42, 0x2f, 0x8a, 0x00, 0xe0, 0x13, 0x60, 0xc2, 0xc6, 0xc7, 0xa6, 0x1d, - 0xda, 0xbc, 0x27, 0xd3, 0x84, 0xd8, 0xb6, 0x20, 0xa1, 0x88, 0xc7, 0x2a, 0x23, 0x39, 0x36, 0xac, - 0x90, 0x9a, 0x2d, 0x22, 0x99, 0x3a, 0xe0, 0xb7, 0xa7, 0xaa, 0x8c, 0x9b, 0x29, 0x3e, 0xea, 0x59, - 0xc1, 0xc1, 0x4c, 0x87, 0x2f, 0x9e, 0x4a, 0x80, 0x09, 0x12, 0x8a, 0x78, 0xdd, 0x60, 0x52, 0x7e, - 0x7a, 0x10, 0x98, 0x5c, 0xdc, 0xb3, 0x02, 0x7e, 0x11, 0x4c, 0xda, 0xf8, 0xf8, 0x1a, 0x71, 0x9a, - 0xc1, 0x81, 0x3e, 0xb3, 0xac, 0xad, 0x66, 0x2b, 0x33, 0x9d, 0x76, 0x71, 0x72, 0x3b, 0x22, 0xa2, - 0x98, 0xcf, 0x85, 0x4d, 0x47, 0x0a, 0x9f, 0x4f, 0x08, 0x47, 0x44, 0x14, 0xf3, 0x59, 0x07, 0xe1, - 0xe1, 0x80, 0x25, 0x97, 0x3e, 0xdb, 0xfd, 0x32, 0xdc, 0x13, 0x64, 0x14, 0xf1, 0xe1, 0x2a, 0xc8, - 0xdb, 0xf8, 0x98, 0xbf, 0xe2, 0xf5, 0x39, 0xae, 0x96, 0xcf, 0x92, 0xb7, 0x25, 0x0d, 0x29, 0x2e, - 0x97, 0x34, 0x1d, 0x21, 0x39, 0x9f, 0x90, 0x94, 0x34, 0xa4, 0xb8, 0x2c, 0x88, 0x43, 0xc7, 0xbc, - 0x15, 0x12, 0x21, 0x0c, 0xb9, 0x67, 0x54, 0x10, 0xdf, 0x88, 0x59, 0x28, 0x29, 0xc7, 0x5e, 0xd1, - 0x76, 0x68, 0x05, 0xa6, 0x67, 0x91, 0xdd, 0x86, 0x7e, 0x81, 0xfb, 0x9f, 0xf7, 0xc9, 0xdb, 0x8a, - 0x8a, 0x12, 0x12, 0x90, 0x80, 0x31, 0xe2, 0x84, 0xb6, 0xfe, 0x08, 0xbf, 0xd8, 0x87, 0x12, 0x82, - 0x2a, 0x73, 0x36, 0x9d, 0xd0, 0x46, 0x5c, 0x3d, 0x7c, 0x1e, 0xcc, 0xd8, 0xf8, 0x98, 0x95, 0x03, - 0xe2, 0x07, 0xec, 0x7d, 0xbf, 0xc0, 0x37, 0x3f, 0xcf, 0x3a, 0xce, 0xed, 0x24, 0x03, 0x75, 0xcb, - 0xf1, 0x85, 0xa6, 0x93, 0x58, 0x78, 0x31, 0xb1, 0x30, 0xc9, 0x40, 0xdd, 0x72, 0xcc, 0xd3, 0x3e, - 0xb9, 0x15, 0x9a, 0x3e, 0xa9, 0xeb, 0x8f, 0xf2, 0x26, 0x55, 0xce, 0xf7, 0x05, 0x0d, 0x29, 0x2e, - 0x6c, 0x45, 0xe3, 0x1e, 0x9d, 0xa7, 0xe1, 0x8d, 0xe1, 0x56, 0xf2, 0x5d, 0x7f, 0xdd, 0xf7, 0xf1, - 0x89, 0xb8, 0x69, 0x92, 0x83, 0x1e, 0x48, 0x41, 0x0e, 0x5b, 0xd6, 0x6e, 0x43, 0xbf, 0xc4, 0x7d, - 0x3f, 0xec, 0x1b, 0x44, 0x55, 0x9d, 0x75, 0x06, 0x82, 0x04, 0x16, 0x03, 0x75, 0x1d, 0x16, 0x1a, - 0x8b, 0xa3, 0x05, 0xdd, 0x65, 0x20, 0x48, 0x60, 0xf1, 0x9d, 0x3a, 0x27, 0xbb, 0x0d, 0xfd, 0xff, - 0x46, 0xbc, 0x53, 0x06, 0x82, 0x04, 0x16, 0x34, 0x41, 0xd6, 0x71, 0x03, 0x7d, 0x69, 0x24, 0xd7, - 0x33, 0xbf, 0x70, 0x76, 0xdc, 0x00, 0x31, 0x0c, 0xf8, 0x0b, 0x0d, 0x00, 0x2f, 0x0e, 0xd1, 0xc7, - 0x86, 0x32, 0x45, 0x48, 0x41, 0x96, 0xe3, 0xd8, 0xde, 0x74, 0x02, 0xff, 0x24, 0x7e, 0x47, 0x26, - 0x72, 0x20, 0x61, 0x05, 0xfc, 0xad, 0x06, 0x1e, 0x49, 0xb6, 0xc9, 0xca, 0xbc, 0x02, 0xf7, 0xc8, - 0xf5, 0x61, 0x87, 0x79, 0xc5, 0x75, 0xad, 0x8a, 0xde, 0x69, 0x17, 0x1f, 0x59, 0xef, 0x83, 0x8a, - 0xfa, 0xda, 0x02, 0xff, 0xa8, 0x81, 0x79, 0x59, 0x45, 0x13, 0x16, 0x16, 0xb9, 0x03, 0xc9, 0xb0, - 0x1d, 0x98, 0xc6, 0x11, 0x7e, 0x54, 0xbf, 0x4b, 0xf7, 0xf0, 0x51, 0xaf, 0x69, 0xf0, 0x2f, 0x1a, - 0x98, 0xae, 0x13, 0x8f, 0x38, 0x75, 0xe2, 0x18, 0xcc, 0xd6, 0xe5, 0xa1, 0x8c, 0x0d, 0xd2, 0xb6, - 0x6e, 0x24, 0x20, 0x84, 0x99, 0x65, 0x69, 0xe6, 0x74, 0x92, 0x75, 0xda, 0x2e, 0x5e, 0x8c, 0x97, - 0x26, 0x39, 0xa8, 0xcb, 0x4a, 0xf8, 0xa1, 0x06, 0x66, 0xe3, 0x03, 0x10, 0x57, 0xca, 0xca, 0x08, - 0xe3, 0x80, 0xb7, 0xaf, 0xeb, 0xdd, 0x80, 0x28, 0x6d, 0x01, 0xfc, 0x93, 0xc6, 0x3a, 0xb5, 0xe8, - 0xdd, 0x47, 0xf5, 0x12, 0xf7, 0xe5, 0x3b, 0x43, 0xf7, 0xa5, 0x42, 0x10, 0xae, 0xbc, 0x1c, 0xb7, - 0x82, 0x8a, 0x73, 0xda, 0x2e, 0x2e, 0x24, 0x3d, 0xa9, 0x18, 0x28, 0x69, 0x21, 0xfc, 0xb1, 0x06, - 0xa6, 0x49, 0xdc, 0x71, 0x53, 0xfd, 0xf1, 0xa1, 0x38, 0xb1, 0x6f, 0x13, 0x2f, 0x5e, 0xea, 0x09, - 0x16, 0x45, 0x5d, 0xd8, 0xac, 0x83, 0x24, 0xc7, 0xd8, 0xf6, 0x2c, 0xa2, 0xff, 0xff, 0x90, 0x3b, - 0xc8, 0x4d, 0xa1, 0x17, 0x45, 0x00, 0xf0, 0x32, 0xc8, 0x3b, 0xa1, 0x65, 0xe1, 0x7d, 0x8b, 0xe8, - 0x4f, 0xf0, 0x5e, 0x44, 0x4d, 0x31, 0x77, 0x24, 0x1d, 0x29, 0x89, 0x45, 0xf6, 0x4e, 0x4a, 0xe5, - 0x19, 0x9c, 0x03, 0xd9, 0x43, 0x22, 0x7f, 0x0e, 0x46, 0xec, 0x4f, 0x58, 0x07, 0xb9, 0x16, 0xb6, - 0xc2, 0xe8, 0xa9, 0x37, 0xe4, 0x1a, 0x8d, 0x84, 0xf2, 0x17, 0x33, 0x2f, 0x68, 0x8b, 0xb7, 0x35, - 0x70, 0xb1, 0x7f, 0xfa, 0x3f, 0x54, 0xb3, 0x7e, 0xad, 0x81, 0xf9, 0x9e, 0x4c, 0xef, 0x63, 0xd1, - 0xad, 0x6e, 0x8b, 0xde, 0x18, 0x76, 0xca, 0xd6, 0x02, 0xdf, 0x74, 0x9a, 0xbc, 0x4f, 0x49, 0x9a, - 0xf7, 0x33, 0x0d, 0xcc, 0xa5, 0x93, 0xe7, 0x61, 0xfa, 0xab, 0x74, 0x3b, 0x03, 0x2e, 0xf6, 0x6f, - 0xaf, 0xa0, 0xaf, 0xde, 0x91, 0xa3, 0x79, 0x8f, 0xf7, 0x9b, 0xdd, 0xbd, 0xaf, 0x81, 0xa9, 0x9b, - 0x4a, 0x2e, 0xfa, 0xb9, 0x70, 0xe8, 0x93, 0x80, 0xa8, 0x5a, 0xc5, 0x0c, 0x8a, 0x92, 0xb8, 0xa5, - 0x3f, 0x6b, 0x60, 0xa1, 0x6f, 0x19, 0x66, 0x0f, 0x56, 0x6c, 0x59, 0xee, 0x91, 0x18, 0xe8, 0x24, - 0xa6, 0xa5, 0xeb, 0x9c, 0x8a, 0x24, 0x37, 0xe1, 0xbd, 0xcc, 0xe7, 0xe5, 0xbd, 0xd2, 0xdf, 0x34, - 0xb0, 0x74, 0xaf, 0x48, 0x7c, 0x28, 0x47, 0xba, 0x0a, 0xf2, 0xb2, 0x85, 0x3a, 0xe1, 0xc7, 0x29, - 0x5f, 0x0d, 0xb2, 0x68, 0xf0, 0xff, 0x90, 0x11, 0x7f, 0x95, 0x3e, 0xd2, 0xc0, 0x5c, 0x8d, 0xf8, - 0x2d, 0xd3, 0x20, 0x88, 0x34, 0x88, 0x4f, 0x1c, 0x83, 0xc0, 0x35, 0x30, 0xc9, 0x7f, 0xa7, 0xf3, - 0xb0, 0x11, 0x0d, 0xb1, 0xe7, 0xa5, 0xcb, 0x27, 0x77, 0x22, 0x06, 0x8a, 0x65, 0xd4, 0xc0, 0x3b, - 0x33, 0x70, 0xe0, 0xbd, 0x04, 0xc6, 0xbc, 0x78, 0x1c, 0x98, 0x67, 0x5c, 0x3e, 0x01, 0xe4, 0x54, - 0xce, 0x75, 0xfd, 0x80, 0xcf, 0x38, 0x72, 0x92, 0xeb, 0xfa, 0x01, 0xe2, 0xd4, 0xd2, 0xdf, 0x35, - 0xd0, 0xef, 0x7f, 0x59, 0x60, 0x0b, 0x4c, 0x50, 0x61, 0xba, 0x74, 0xed, 0xee, 0x03, 0xba, 0x36, - 0xed, 0x08, 0x71, 0x89, 0x44, 0xd4, 0x08, 0x8c, 0x79, 0xd7, 0xc0, 0x95, 0xd0, 0xa9, 0xcb, 0xf1, - 0xde, 0xb4, 0xf0, 0x6e, 0x75, 0x5d, 0xd0, 0x90, 0xe2, 0xc2, 0x4b, 0x62, 0x10, 0x95, 0x98, 0xee, - 0x44, 0x43, 0xa8, 0xca, 0x95, 0x3b, 0x77, 0x0b, 0xe7, 0x3e, 0xbe, 0x5b, 0x38, 0xf7, 0xc9, 0xdd, - 0xc2, 0xb9, 0xef, 0x76, 0x0a, 0xda, 0x9d, 0x4e, 0x41, 0xfb, 0xb8, 0x53, 0xd0, 0x3e, 0xe9, 0x14, - 0xb4, 0x7f, 0x75, 0x0a, 0xda, 0xcf, 0x3f, 0x2d, 0x9c, 0xfb, 0xe6, 0x84, 0x34, 0xed, 0xbf, 0x01, - 0x00, 0x00, 0xff, 0xff, 0xd1, 0xda, 0xdd, 0xd0, 0x61, 0x2a, 0x00, 0x00, + // 2884 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xcc, 0x5a, 0xcd, 0x73, 0x1c, 0x47, + 0x15, 0xf7, 0xac, 0xb4, 0xd2, 0xaa, 0x25, 0x59, 0x52, 0xdb, 0x56, 0xc6, 0x8a, 0xb3, 0x2b, 0x6f, + 0x48, 0x10, 0xc1, 0x5e, 0x25, 0x26, 0x21, 0x21, 0x55, 0x1c, 0xb4, 0x92, 0x92, 0x52, 0x62, 0x7d, + 0xd0, 0x6b, 0x27, 0x86, 0x7c, 0xb6, 0x76, 0x5a, 0xab, 0xb1, 0xe6, 0xcb, 0xdd, 0x33, 0x2b, 0xa9, + 0x02, 0x14, 0x1f, 0x95, 0x82, 0xa2, 0x80, 0x50, 0x24, 0x17, 0x0a, 0x38, 0x04, 0x8a, 0x0b, 0x07, + 0x38, 0xc0, 0x0d, 0xfe, 0x80, 0x1c, 0x53, 0x9c, 0x72, 0xa0, 0xb6, 0xf0, 0xe6, 0x2f, 0xa0, 0x8a, + 0x2a, 0xaa, 0x74, 0xa2, 0xfa, 0x63, 0x7a, 0x66, 0x67, 0x77, 0x6d, 0x57, 0xbc, 0x1b, 0x73, 0xd3, + 0xbc, 0xaf, 0xdf, 0xeb, 0xd7, 0xaf, 0x5f, 0xbf, 0x7e, 0x2b, 0xb0, 0x77, 0xf0, 0x1c, 0xab, 0xd8, + 0xfe, 0xf2, 0x41, 0xb4, 0x4b, 0xa8, 0x47, 0x42, 0xc2, 0x96, 0x9b, 0xc4, 0xb3, 0x7c, 0xba, 0xac, + 0x18, 0x38, 0xb0, 0xc9, 0x51, 0x48, 0x3c, 0x66, 0xfb, 0x1e, 0xbb, 0x8c, 0x03, 0x9b, 0x11, 0xda, + 0x24, 0x74, 0x39, 0x38, 0x68, 0x70, 0x1e, 0xeb, 0x14, 0x58, 0x6e, 0x3e, 0xb5, 0x4b, 0x42, 0xfc, + 0xd4, 0x72, 0x83, 0x78, 0x84, 0xe2, 0x90, 0x58, 0x95, 0x80, 0xfa, 0xa1, 0x0f, 0xbf, 0x2e, 0xcd, + 0x55, 0x3a, 0xa4, 0xdf, 0xd2, 0xe6, 0x2a, 0xc1, 0x41, 0x83, 0xf3, 0x58, 0xa7, 0x40, 0x45, 0x99, + 0x5b, 0xb8, 0xdc, 0xb0, 0xc3, 0xfd, 0x68, 0xb7, 0x52, 0xf7, 0xdd, 0xe5, 0x86, 0xdf, 0xf0, 0x97, + 0x85, 0xd5, 0xdd, 0x68, 0x4f, 0x7c, 0x89, 0x0f, 0xf1, 0x97, 0x44, 0x5b, 0x78, 0x3a, 0x71, 0xde, + 0xc5, 0xf5, 0x7d, 0xdb, 0x23, 0xf4, 0x38, 0xf1, 0xd8, 0x25, 0x21, 0x5e, 0x6e, 0x76, 0xf9, 0xb8, + 0xb0, 0xdc, 0x4f, 0x8b, 0x46, 0x5e, 0x68, 0xbb, 0xa4, 0x4b, 0xe1, 0xab, 0x77, 0x53, 0x60, 0xf5, + 0x7d, 0xe2, 0xe2, 0xac, 0x5e, 0xf9, 0xc4, 0x00, 0x73, 0xab, 0xbe, 0xd7, 0x24, 0x94, 0xaf, 0x12, + 0x91, 0x5b, 0x11, 0x61, 0x21, 0xac, 0x82, 0x91, 0xc8, 0xb6, 0x4c, 0x63, 0xd1, 0x58, 0x9a, 0xa8, + 0x3e, 0xf9, 0x51, 0xab, 0x74, 0xaa, 0xdd, 0x2a, 0x8d, 0x5c, 0xdf, 0x58, 0x3b, 0x69, 0x95, 0x2e, + 0xf6, 0x43, 0x0a, 0x8f, 0x03, 0xc2, 0x2a, 0xd7, 0x37, 0xd6, 0x10, 0x57, 0x86, 0x2f, 0x82, 0x39, + 0x8b, 0x30, 0x9b, 0x12, 0x6b, 0x65, 0x67, 0xe3, 0x15, 0x69, 0xdf, 0xcc, 0x09, 0x8b, 0xe7, 0x95, + 0xc5, 0xb9, 0xb5, 0xac, 0x00, 0xea, 0xd6, 0x81, 0x37, 0xc0, 0xb8, 0xbf, 0x7b, 0x93, 0xd4, 0x43, + 0x66, 0x8e, 0x2c, 0x8e, 0x2c, 0x4d, 0x5e, 0xb9, 0x5c, 0x49, 0x76, 0x50, 0xbb, 0x20, 0xb6, 0x4d, + 0x2d, 0xb6, 0x82, 0xf0, 0xe1, 0x7a, 0xbc, 0x73, 0xd5, 0x19, 0x85, 0x36, 0xbe, 0x2d, 0xad, 0xa0, + 0xd8, 0x5c, 0xf9, 0xf7, 0x39, 0x00, 0xd3, 0x8b, 0x67, 0x81, 0xef, 0x31, 0x32, 0x90, 0xd5, 0x33, + 0x30, 0x5b, 0x17, 0x96, 0x43, 0x62, 0x29, 0x5c, 0x33, 0xf7, 0x59, 0xbc, 0x37, 0x15, 0xfe, 0xec, + 0x6a, 0xc6, 0x1c, 0xea, 0x02, 0x80, 0xd7, 0xc0, 0x18, 0x25, 0x2c, 0x72, 0x42, 0x73, 0x64, 0xd1, + 0x58, 0x9a, 0xbc, 0x72, 0xa9, 0x2f, 0x94, 0xc8, 0x6f, 0x9e, 0x7c, 0x95, 0xe6, 0x53, 0x95, 0x5a, + 0x88, 0xc3, 0x88, 0x55, 0x4f, 0x2b, 0xa4, 0x31, 0x24, 0x6c, 0x20, 0x65, 0xab, 0xfc, 0xe3, 0x1c, + 0x98, 0x4d, 0x47, 0xa9, 0x69, 0x93, 0x43, 0x78, 0x08, 0xc6, 0xa9, 0x4c, 0x16, 0x11, 0xa7, 0xc9, + 0x2b, 0x3b, 0x95, 0xfb, 0x3a, 0x56, 0x95, 0xae, 0x24, 0xac, 0x4e, 0xf2, 0x3d, 0x53, 0x1f, 0x28, + 0x46, 0x83, 0xef, 0x80, 0x02, 0x55, 0x1b, 0x25, 0xb2, 0x69, 0xf2, 0xca, 0x37, 0x06, 0x88, 0x2c, + 0x0d, 0x57, 0xa7, 0xda, 0xad, 0x52, 0x21, 0xfe, 0x42, 0x1a, 0xb0, 0xfc, 0x7e, 0x0e, 0x14, 0x57, + 0x23, 0x16, 0xfa, 0x2e, 0x22, 0xcc, 0x8f, 0x68, 0x9d, 0xac, 0xfa, 0x4e, 0xe4, 0x7a, 0x6b, 0x64, + 0xcf, 0xf6, 0xec, 0x90, 0x67, 0xeb, 0x22, 0x18, 0xf5, 0xb0, 0x4b, 0x54, 0xf6, 0x4c, 0xa9, 0x98, + 0x8e, 0x6e, 0x61, 0x97, 0x20, 0xc1, 0xe1, 0x12, 0x3c, 0x59, 0xd4, 0x59, 0xd0, 0x12, 0xd7, 0x8e, + 0x03, 0x82, 0x04, 0x07, 0x3e, 0x0e, 0xc6, 0xf6, 0x7c, 0xea, 0x62, 0xb9, 0x8f, 0x13, 0xc9, 0xce, + 0xbc, 0x20, 0xa8, 0x48, 0x71, 0xe1, 0x33, 0x60, 0xd2, 0x22, 0xac, 0x4e, 0xed, 0x80, 0x43, 0x9b, + 0xa3, 0x42, 0xf8, 0x8c, 0x12, 0x9e, 0x5c, 0x4b, 0x58, 0x28, 0x2d, 0x07, 0x2f, 0x81, 0x42, 0x40, + 0x6d, 0x9f, 0xda, 0xe1, 0xb1, 0x99, 0x5f, 0x34, 0x96, 0xf2, 0xd5, 0x59, 0xa5, 0x53, 0xd8, 0x51, + 0x74, 0xa4, 0x25, 0xe0, 0x22, 0x28, 0xbc, 0x54, 0xdb, 0xde, 0xda, 0xc1, 0xe1, 0xbe, 0x39, 0x26, + 0x10, 0x46, 0xb9, 0x34, 0x2a, 0xdc, 0x54, 0xd4, 0xf2, 0x3f, 0x73, 0xc0, 0xcc, 0x46, 0x25, 0x0e, + 0x29, 0x7c, 0x01, 0x14, 0x58, 0xc8, 0x2b, 0x4e, 0xe3, 0x58, 0xc5, 0xe4, 0x89, 0x18, 0xac, 0xa6, + 0xe8, 0x27, 0xad, 0xd2, 0x7c, 0xa2, 0x11, 0x53, 0x45, 0x3c, 0xb4, 0x2e, 0xfc, 0xad, 0x01, 0xce, + 0x1c, 0x92, 0xdd, 0x7d, 0xdf, 0x3f, 0x58, 0x75, 0x6c, 0xe2, 0x85, 0xab, 0xbe, 0xb7, 0x67, 0x37, + 0x54, 0x0e, 0xa0, 0xfb, 0xcc, 0x81, 0x57, 0xbb, 0x2d, 0x57, 0x1f, 0x6a, 0xb7, 0x4a, 0x67, 0x7a, + 0x30, 0x50, 0x2f, 0x3f, 0xe0, 0x0d, 0x60, 0xd6, 0x33, 0x87, 0x44, 0x15, 0x30, 0x59, 0xb6, 0x26, + 0xaa, 0x17, 0xda, 0xad, 0x92, 0xb9, 0xda, 0x47, 0x06, 0xf5, 0xd5, 0x2e, 0xff, 0x70, 0x24, 0x1b, + 0xde, 0x54, 0xba, 0xbd, 0x0d, 0x0a, 0xfc, 0x18, 0x5b, 0x38, 0xc4, 0xea, 0x20, 0x3e, 0x79, 0x6f, + 0x87, 0x5e, 0xd6, 0x8c, 0x4d, 0x12, 0xe2, 0x2a, 0x54, 0x1b, 0x02, 0x12, 0x1a, 0xd2, 0x56, 0xe1, + 0x77, 0xc0, 0x28, 0x0b, 0x48, 0x5d, 0x05, 0xfa, 0xb5, 0xfb, 0x3d, 0x6c, 0x7d, 0x16, 0x52, 0x0b, + 0x48, 0x3d, 0x39, 0x0b, 0xfc, 0x0b, 0x09, 0x58, 0xf8, 0xae, 0x01, 0xc6, 0x98, 0x28, 0x50, 0xaa, + 0xa8, 0xbd, 0x31, 0x2c, 0x0f, 0x32, 0x55, 0x50, 0x7e, 0x23, 0x05, 0x5e, 0xfe, 0x4f, 0x0e, 0x5c, + 0xec, 0xa7, 0xba, 0xea, 0x7b, 0x96, 0xdc, 0x8e, 0x0d, 0x75, 0xb6, 0x65, 0xa6, 0x3f, 0x93, 0x3e, + 0xdb, 0x27, 0xad, 0xd2, 0x63, 0x77, 0x35, 0x90, 0x2a, 0x02, 0x5f, 0xd3, 0xeb, 0x96, 0x85, 0xe2, + 0x62, 0xa7, 0x63, 0x27, 0xad, 0xd2, 0x8c, 0x56, 0xeb, 0xf4, 0x15, 0x36, 0x01, 0x74, 0x30, 0x0b, + 0xaf, 0x51, 0xec, 0x31, 0x69, 0xd6, 0x76, 0x89, 0x0a, 0xdf, 0x13, 0xf7, 0x96, 0x1e, 0x5c, 0xa3, + 0xba, 0xa0, 0x20, 0xe1, 0xd5, 0x2e, 0x6b, 0xa8, 0x07, 0x02, 0xaf, 0x5b, 0x94, 0x60, 0xa6, 0x4b, + 0x51, 0xea, 0x46, 0xe1, 0x54, 0xa4, 0xb8, 0xf0, 0x4b, 0x60, 0xdc, 0x25, 0x8c, 0xe1, 0x06, 0x11, + 0xf5, 0x67, 0x22, 0xb9, 0xa2, 0x37, 0x25, 0x19, 0xc5, 0x7c, 0xde, 0x9f, 0x5c, 0xe8, 0x17, 0xb5, + 0xab, 0x36, 0x0b, 0xe1, 0xeb, 0x5d, 0x07, 0xa0, 0x72, 0x6f, 0x2b, 0xe4, 0xda, 0x22, 0xfd, 0x75, + 0xf1, 0x8b, 0x29, 0xa9, 0xe4, 0xff, 0x36, 0xc8, 0xdb, 0x21, 0x71, 0xe3, 0xbb, 0xfb, 0xd5, 0x21, + 0xe5, 0x5e, 0x75, 0x5a, 0xf9, 0x90, 0xdf, 0xe0, 0x68, 0x48, 0x82, 0x96, 0xff, 0x90, 0x03, 0x8f, + 0xf4, 0x53, 0xe1, 0x17, 0x0a, 0xe3, 0x11, 0x0f, 0x9c, 0x88, 0x62, 0x47, 0x65, 0x9c, 0x8e, 0xf8, + 0x8e, 0xa0, 0x22, 0xc5, 0xe5, 0x25, 0x9f, 0xd9, 0x5e, 0x23, 0x72, 0x30, 0x55, 0xe9, 0xa4, 0x57, + 0x5d, 0x53, 0x74, 0xa4, 0x25, 0x60, 0x05, 0x00, 0xb6, 0xef, 0xd3, 0x50, 0x60, 0xa8, 0xea, 0x75, + 0x9a, 0x17, 0x88, 0x9a, 0xa6, 0xa2, 0x94, 0x04, 0xbf, 0xd1, 0x0e, 0x6c, 0xcf, 0x52, 0xbb, 0xae, + 0x4f, 0xf1, 0xcb, 0xb6, 0x67, 0x21, 0xc1, 0xe1, 0xf8, 0x8e, 0xcd, 0x42, 0x4e, 0x51, 0x5b, 0xde, + 0x11, 0x75, 0x21, 0xa9, 0x25, 0x38, 0x7e, 0x9d, 0x57, 0x7d, 0x9f, 0xda, 0x84, 0x99, 0x63, 0x09, + 0xfe, 0xaa, 0xa6, 0xa2, 0x94, 0x44, 0xf9, 0xd7, 0x85, 0xfe, 0x49, 0xc2, 0x4b, 0x09, 0x7c, 0x14, + 0xe4, 0x1b, 0xd4, 0x8f, 0x02, 0x15, 0x25, 0x1d, 0xed, 0x17, 0x39, 0x11, 0x49, 0x1e, 0xcf, 0xca, + 0x66, 0x47, 0x9b, 0xaa, 0xb3, 0x32, 0x6e, 0x4e, 0x63, 0x3e, 0xfc, 0xbe, 0x01, 0xf2, 0x9e, 0x0a, + 0x0e, 0x4f, 0xb9, 0xd7, 0x87, 0x94, 0x17, 0x22, 0xbc, 0x89, 0xbb, 0x32, 0xf2, 0x12, 0x19, 0x3e, + 0x0d, 0xf2, 0xac, 0xee, 0x07, 0x44, 0x45, 0xbd, 0x18, 0x0b, 0xd5, 0x38, 0xf1, 0xa4, 0x55, 0x9a, + 0x8e, 0xcd, 0x09, 0x02, 0x92, 0xc2, 0xf0, 0x47, 0x06, 0x00, 0x4d, 0xec, 0xd8, 0x16, 0x16, 0x2d, + 0x43, 0x5e, 0xb8, 0x3f, 0xd8, 0xb4, 0x7e, 0x45, 0x9b, 0x97, 0x9b, 0x96, 0x7c, 0xa3, 0x14, 0x34, + 0x7c, 0xcf, 0x00, 0x53, 0x2c, 0xda, 0xa5, 0x4a, 0x8b, 0x89, 0xe6, 0x62, 0xf2, 0xca, 0x37, 0x07, + 0xea, 0x4b, 0x2d, 0x05, 0x50, 0x9d, 0x6d, 0xb7, 0x4a, 0x53, 0x69, 0x0a, 0xea, 0x70, 0x00, 0xfe, + 0xd4, 0x00, 0x85, 0x66, 0x7c, 0x67, 0x8f, 0x8b, 0x03, 0xff, 0xe6, 0x90, 0x36, 0x56, 0x65, 0x54, + 0x72, 0x0a, 0x74, 0x1f, 0xa0, 0x3d, 0x80, 0x7f, 0x33, 0x80, 0x89, 0x2d, 0x59, 0xe0, 0xb1, 0xb3, + 0x43, 0x6d, 0x2f, 0x24, 0x54, 0xf6, 0x9b, 0xcc, 0x2c, 0x08, 0xf7, 0x06, 0x7b, 0x17, 0x66, 0x7b, + 0xd9, 0xea, 0xa2, 0xf2, 0xce, 0x5c, 0xe9, 0xe3, 0x06, 0xea, 0xeb, 0xa0, 0x48, 0xb4, 0xa4, 0xa5, + 0x31, 0x27, 0x86, 0x90, 0x68, 0x49, 0x2f, 0xa5, 0xaa, 0x43, 0xd2, 0x41, 0xa5, 0xa0, 0xcb, 0xef, + 0x8d, 0x64, 0x9b, 0xf6, 0xec, 0xa5, 0x0f, 0x3f, 0x90, 0xce, 0xca, 0xa5, 0x30, 0xd3, 0x10, 0xc1, + 0x7d, 0x7b, 0x48, 0x7b, 0xaf, 0x6f, 0xed, 0xa4, 0xf1, 0xd2, 0x24, 0x86, 0x52, 0x7e, 0xc0, 0x5f, + 0x19, 0x60, 0x1a, 0xd7, 0xeb, 0x24, 0x08, 0x89, 0x25, 0x6b, 0x71, 0xee, 0x73, 0x28, 0x37, 0xe7, + 0x94, 0x57, 0xd3, 0x2b, 0x69, 0x68, 0xd4, 0xe9, 0x09, 0x7c, 0x1e, 0x9c, 0x66, 0xa1, 0x4f, 0x89, + 0x95, 0xe9, 0x72, 0x61, 0xbb, 0x55, 0x3a, 0x5d, 0xeb, 0xe0, 0xa0, 0x8c, 0x64, 0xf9, 0xd3, 0x51, + 0x50, 0xba, 0xcb, 0xc9, 0xb8, 0x87, 0x77, 0xd4, 0xe3, 0x60, 0x4c, 0x2c, 0xd7, 0x12, 0x51, 0x29, + 0xa4, 0x3a, 0x37, 0x41, 0x45, 0x8a, 0xcb, 0xeb, 0x3a, 0xc7, 0xe7, 0xdd, 0xc6, 0x88, 0x10, 0xd4, + 0x75, 0xbd, 0x26, 0xc9, 0x28, 0xe6, 0xc3, 0x77, 0xc0, 0x98, 0x9c, 0x93, 0x88, 0xa2, 0x3a, 0xc4, + 0xc2, 0x08, 0x84, 0x9f, 0x02, 0x0a, 0x29, 0xc8, 0xee, 0x82, 0x98, 0x7f, 0xd0, 0x05, 0xf1, 0x8e, + 0x15, 0x68, 0xec, 0xff, 0xbc, 0x02, 0x95, 0xff, 0x6b, 0x64, 0xcf, 0x7d, 0x6a, 0xa9, 0xb5, 0x3a, + 0x76, 0x08, 0x5c, 0x03, 0xb3, 0xfc, 0x91, 0x81, 0x48, 0xe0, 0xd8, 0x75, 0xcc, 0xc4, 0x1b, 0x57, + 0x26, 0x9c, 0x1e, 0xbb, 0xd4, 0x32, 0x7c, 0xd4, 0xa5, 0x01, 0x5f, 0x02, 0x50, 0x36, 0xde, 0x1d, + 0x76, 0x64, 0x0f, 0xa1, 0x5b, 0xe8, 0x5a, 0x97, 0x04, 0xea, 0xa1, 0x05, 0x57, 0xc1, 0x9c, 0x83, + 0x77, 0x89, 0x53, 0x23, 0x0e, 0xa9, 0x87, 0x3e, 0x15, 0xa6, 0xe4, 0x14, 0xe0, 0x5c, 0xbb, 0x55, + 0x9a, 0xbb, 0x9a, 0x65, 0xa2, 0x6e, 0xf9, 0xf2, 0xc5, 0xec, 0xf1, 0x4a, 0x2f, 0x5c, 0x3e, 0x67, + 0x3e, 0xcc, 0x81, 0x85, 0xfe, 0x99, 0x01, 0x7f, 0x90, 0xbc, 0xba, 0x64, 0x53, 0xfd, 0xe6, 0xb0, + 0xb2, 0x50, 0x3d, 0xbb, 0x40, 0xf7, 0x93, 0x0b, 0x7e, 0x97, 0x77, 0x38, 0xd8, 0x89, 0xe7, 0x3c, + 0x6f, 0x0c, 0xcd, 0x05, 0x0e, 0x52, 0x9d, 0x90, 0xcd, 0x13, 0x76, 0x44, 0xaf, 0x84, 0x1d, 0x52, + 0xfe, 0xa3, 0x91, 0x7d, 0x78, 0x27, 0x27, 0x18, 0xfe, 0xcc, 0x00, 0x33, 0x7e, 0x40, 0xbc, 0x95, + 0x9d, 0x8d, 0x57, 0xbe, 0x22, 0x4f, 0xb2, 0x0a, 0xd5, 0xd6, 0x7d, 0xfa, 0xf9, 0x52, 0x6d, 0x7b, + 0x4b, 0x1a, 0xdc, 0xa1, 0x7e, 0xc0, 0xaa, 0x67, 0xda, 0xad, 0xd2, 0xcc, 0x76, 0x27, 0x14, 0xca, + 0x62, 0x97, 0x5d, 0x70, 0x6e, 0xfd, 0x28, 0x24, 0xd4, 0xc3, 0xce, 0x9a, 0x5f, 0x8f, 0x5c, 0xe2, + 0x85, 0xd2, 0xd1, 0xcc, 0x90, 0xc8, 0xb8, 0xc7, 0x21, 0xd1, 0x23, 0x60, 0x24, 0xa2, 0x8e, 0xca, + 0xe2, 0x49, 0x3d, 0x04, 0x45, 0x57, 0x11, 0xa7, 0x97, 0x2f, 0x82, 0x51, 0xee, 0x27, 0x3c, 0x0f, + 0x46, 0x28, 0x3e, 0x14, 0x56, 0xa7, 0xaa, 0xe3, 0x5c, 0x04, 0xe1, 0x43, 0xc4, 0x69, 0xe5, 0x7f, + 0x17, 0xc1, 0x4c, 0x66, 0x2d, 0x70, 0x01, 0xe4, 0xf4, 0x64, 0x15, 0x28, 0xa3, 0xb9, 0x8d, 0x35, + 0x94, 0xb3, 0x2d, 0xf8, 0xac, 0x2e, 0xbe, 0x12, 0xb4, 0xa4, 0xeb, 0xb9, 0xa0, 0xf2, 0x96, 0x36, + 0x31, 0xc7, 0x1d, 0x89, 0x0b, 0x27, 0xf7, 0x81, 0xec, 0xa9, 0x53, 0x22, 0x7d, 0x20, 0x7b, 0x88, + 0xd3, 0x3e, 0xeb, 0x84, 0x2c, 0x1e, 0xd1, 0xe5, 0xef, 0x61, 0x44, 0x37, 0x76, 0xc7, 0x11, 0xdd, + 0xa3, 0x20, 0x1f, 0xda, 0xa1, 0x43, 0xcc, 0xf1, 0xce, 0x97, 0xc7, 0x35, 0x4e, 0x44, 0x92, 0x07, + 0x6f, 0x82, 0x71, 0x8b, 0xec, 0xe1, 0xc8, 0x09, 0xcd, 0x82, 0x48, 0xa1, 0xd5, 0x01, 0xa4, 0x90, + 0x9c, 0x9f, 0xae, 0x49, 0xbb, 0x28, 0x06, 0x80, 0x8f, 0x81, 0x71, 0x17, 0x1f, 0xd9, 0x6e, 0xe4, + 0x8a, 0x9e, 0xcc, 0x90, 0x62, 0x9b, 0x92, 0x84, 0x62, 0x1e, 0xaf, 0x8c, 0xe4, 0xa8, 0xee, 0x44, + 0xcc, 0x6e, 0x12, 0xc5, 0x34, 0x81, 0xb8, 0x3d, 0x75, 0x65, 0x5c, 0xcf, 0xf0, 0x51, 0x97, 0x86, + 0x00, 0xb3, 0x3d, 0xa1, 0x3c, 0x99, 0x02, 0x93, 0x24, 0x14, 0xf3, 0x3a, 0xc1, 0x94, 0xfc, 0x54, + 0x3f, 0x30, 0xa5, 0xdc, 0xa5, 0x01, 0xbf, 0x0c, 0x26, 0x5c, 0x7c, 0x74, 0x95, 0x78, 0x8d, 0x70, + 0xdf, 0x9c, 0x5e, 0x34, 0x96, 0x46, 0xaa, 0xd3, 0xed, 0x56, 0x69, 0x62, 0x33, 0x26, 0xa2, 0x84, + 0x2f, 0x84, 0x6d, 0x4f, 0x09, 0x9f, 0x4e, 0x09, 0xc7, 0x44, 0x94, 0xf0, 0x79, 0x07, 0x11, 0xe0, + 0x90, 0x1f, 0x2e, 0x73, 0xa6, 0xf3, 0x65, 0xb8, 0x23, 0xc9, 0x28, 0xe6, 0xc3, 0x25, 0x50, 0x70, + 0xf1, 0x91, 0x78, 0xc5, 0x9b, 0xb3, 0xc2, 0xac, 0x98, 0x25, 0x6f, 0x2a, 0x1a, 0xd2, 0x5c, 0x21, + 0x69, 0x7b, 0x52, 0x72, 0x2e, 0x25, 0xa9, 0x68, 0x48, 0x73, 0x79, 0x12, 0x47, 0x9e, 0x7d, 0x2b, + 0x22, 0x52, 0x18, 0x8a, 0xc8, 0xe8, 0x24, 0xbe, 0x9e, 0xb0, 0x50, 0x5a, 0x8e, 0xbf, 0xa2, 0xdd, + 0xc8, 0x09, 0xed, 0xc0, 0x21, 0xdb, 0x7b, 0xe6, 0x19, 0x11, 0x7f, 0xd1, 0x27, 0x6f, 0x6a, 0x2a, + 0x4a, 0x49, 0x40, 0x02, 0x46, 0x89, 0x17, 0xb9, 0xe6, 0x59, 0x71, 0xb1, 0x0f, 0x24, 0x05, 0xf5, + 0xc9, 0x59, 0xf7, 0x22, 0x17, 0x09, 0xf3, 0xf0, 0x59, 0x30, 0xed, 0xe2, 0x23, 0x5e, 0x0e, 0x08, + 0x0d, 0xf9, 0xfb, 0xfe, 0x9c, 0x58, 0xfc, 0x1c, 0xef, 0x38, 0x37, 0xd3, 0x0c, 0xd4, 0x29, 0x27, + 0x14, 0x6d, 0x2f, 0xa5, 0x38, 0x9f, 0x52, 0x4c, 0x33, 0x50, 0xa7, 0x1c, 0x8f, 0x34, 0x25, 0xb7, + 0x22, 0x9b, 0x12, 0xcb, 0x7c, 0x48, 0x34, 0xa9, 0x6a, 0xbe, 0x2f, 0x69, 0x48, 0x73, 0x61, 0x33, + 0x1e, 0xf7, 0x98, 0xe2, 0x18, 0x5e, 0x1f, 0x6c, 0x25, 0xdf, 0xa6, 0x2b, 0x94, 0xe2, 0x63, 0x79, + 0xd3, 0xa4, 0x07, 0x3d, 0x90, 0x81, 0x3c, 0x76, 0x9c, 0xed, 0x3d, 0xf3, 0xbc, 0x88, 0xfd, 0xa0, + 0x6f, 0x10, 0x5d, 0x75, 0x56, 0x38, 0x08, 0x92, 0x58, 0x1c, 0xd4, 0xf7, 0x78, 0x6a, 0x2c, 0x0c, + 0x17, 0x74, 0x9b, 0x83, 0x20, 0x89, 0x25, 0x56, 0xea, 0x1d, 0x6f, 0xef, 0x99, 0x0f, 0x0f, 0x79, + 0xa5, 0x1c, 0x04, 0x49, 0x2c, 0x68, 0x83, 0x11, 0xcf, 0x0f, 0xcd, 0x0b, 0x43, 0xb9, 0x9e, 0xc5, + 0x85, 0xb3, 0xe5, 0x87, 0x88, 0x63, 0xc0, 0x5f, 0x1a, 0x00, 0x04, 0x49, 0x8a, 0x3e, 0x32, 0x90, + 0x29, 0x42, 0x06, 0xb2, 0x92, 0xe4, 0xf6, 0xba, 0x17, 0xd2, 0xe3, 0xe4, 0x1d, 0x99, 0x3a, 0x03, + 0x29, 0x2f, 0xe0, 0xef, 0x0c, 0x70, 0x36, 0xdd, 0x26, 0x6b, 0xf7, 0x8a, 0x22, 0x22, 0xd7, 0x06, + 0x9d, 0xe6, 0x55, 0xdf, 0x77, 0xaa, 0x66, 0xbb, 0x55, 0x3a, 0xbb, 0xd2, 0x03, 0x15, 0xf5, 0xf4, + 0x05, 0xfe, 0xc9, 0x00, 0x73, 0xaa, 0x8a, 0xa6, 0x3c, 0x2c, 0x89, 0x00, 0x92, 0x41, 0x07, 0x30, + 0x8b, 0x23, 0xe3, 0xa8, 0x7f, 0x97, 0xee, 0xe2, 0xa3, 0x6e, 0xd7, 0xe0, 0x5f, 0x0d, 0x30, 0x65, + 0x91, 0x80, 0x78, 0x16, 0xf1, 0xea, 0xdc, 0xd7, 0xc5, 0x81, 0x8c, 0x0d, 0xb2, 0xbe, 0xae, 0xa5, + 0x20, 0xa4, 0x9b, 0x15, 0xe5, 0xe6, 0x54, 0x9a, 0x75, 0xd2, 0x2a, 0xcd, 0x27, 0xaa, 0x69, 0x0e, + 0xea, 0xf0, 0x12, 0xbe, 0x6f, 0x80, 0x99, 0x64, 0x03, 0xe4, 0x95, 0x72, 0x71, 0x88, 0x79, 0x20, + 0xda, 0xd7, 0x95, 0x4e, 0x40, 0x94, 0xf5, 0x00, 0xfe, 0xd9, 0xe0, 0x9d, 0x5a, 0xfc, 0xee, 0x63, + 0x66, 0x59, 0xc4, 0xf2, 0xad, 0x81, 0xc7, 0x52, 0x23, 0xc8, 0x50, 0x5e, 0x4a, 0x5a, 0x41, 0xcd, + 0x39, 0x69, 0x95, 0xce, 0xa5, 0x23, 0xa9, 0x19, 0x28, 0xed, 0x21, 0xfc, 0x89, 0x01, 0xa6, 0x48, + 0xd2, 0x71, 0x33, 0xf3, 0xd1, 0x81, 0x04, 0xb1, 0x67, 0x13, 0x2f, 0x5f, 0xea, 0x29, 0x16, 0x43, + 0x1d, 0xd8, 0xbc, 0x83, 0x24, 0x47, 0xd8, 0x0d, 0x1c, 0x62, 0x7e, 0x61, 0xc0, 0x1d, 0xe4, 0xba, + 0xb4, 0x8b, 0x62, 0x00, 0x78, 0x09, 0x14, 0xbc, 0xc8, 0x71, 0xf0, 0xae, 0x43, 0xcc, 0xc7, 0x44, + 0x2f, 0xa2, 0xa7, 0x98, 0x5b, 0x8a, 0x8e, 0xb4, 0x04, 0xbc, 0x09, 0x16, 0x8f, 0x5e, 0xd6, 0xff, + 0xd1, 0xb3, 0x43, 0x89, 0xc0, 0xbf, 0xee, 0x1d, 0x78, 0xfe, 0xa1, 0xf7, 0x82, 0x4d, 0x1c, 0x8b, + 0x99, 0x8f, 0x0b, 0x2b, 0xf1, 0x04, 0x7b, 0xfe, 0x46, 0x4f, 0x29, 0x74, 0x57, 0x3b, 0xf0, 0x35, + 0xf0, 0x70, 0x4a, 0x66, 0xdd, 0xdd, 0x25, 0x96, 0x45, 0xac, 0xf8, 0xf1, 0x66, 0x7e, 0x51, 0xc0, + 0xe8, 0x43, 0x7e, 0x23, 0x2b, 0x80, 0xee, 0xa4, 0x0d, 0xaf, 0x82, 0xf9, 0x14, 0x7b, 0xc3, 0x0b, + 0xb7, 0x69, 0x2d, 0xa4, 0xb6, 0xd7, 0x30, 0x97, 0x84, 0xdd, 0xb3, 0xf1, 0xa9, 0xbc, 0x91, 0xe2, + 0xa1, 0x3e, 0x3a, 0x0b, 0xfc, 0xf9, 0x98, 0x29, 0x3f, 0x70, 0x16, 0x8c, 0x1c, 0x10, 0xf5, 0x2b, + 0x39, 0xe2, 0x7f, 0x42, 0x0b, 0xe4, 0x9b, 0xd8, 0x89, 0xe2, 0x17, 0xf0, 0x80, 0xaf, 0x2e, 0x24, + 0x8d, 0x3f, 0x9f, 0x7b, 0xce, 0x58, 0xf8, 0xc0, 0x00, 0xf3, 0xbd, 0xab, 0xe2, 0x03, 0x75, 0xeb, + 0x37, 0x06, 0x98, 0xeb, 0x2a, 0x80, 0x3d, 0x3c, 0xba, 0xd5, 0xe9, 0xd1, 0x6b, 0x83, 0xae, 0x64, + 0x72, 0xd7, 0x44, 0xfb, 0x96, 0x76, 0xef, 0xe7, 0x06, 0x98, 0xcd, 0xd6, 0x94, 0x07, 0x19, 0xaf, + 0xf2, 0x07, 0x39, 0x30, 0xdf, 0xbb, 0xeb, 0x84, 0x54, 0x3f, 0xaf, 0x87, 0x33, 0xa6, 0xe8, 0x35, + 0xd2, 0x7c, 0xd7, 0x00, 0x93, 0x37, 0xb5, 0x5c, 0xfc, 0x2b, 0xea, 0xc0, 0x07, 0x24, 0x71, 0x11, + 0x4f, 0x18, 0x0c, 0xa5, 0x71, 0xcb, 0x7f, 0x31, 0xc0, 0xb9, 0x9e, 0xb7, 0x13, 0x7f, 0xc7, 0x63, + 0xc7, 0xf1, 0x0f, 0xe5, 0x9c, 0x2b, 0x35, 0x44, 0x5e, 0x11, 0x54, 0xa4, 0xb8, 0xa9, 0xe8, 0xe5, + 0x3e, 0xaf, 0xe8, 0x95, 0xff, 0x6e, 0x80, 0x0b, 0x77, 0xca, 0xc4, 0x07, 0xb2, 0xa5, 0x4b, 0xa0, + 0xa0, 0x3a, 0xcb, 0x63, 0xb1, 0x9d, 0xea, 0x31, 0xa5, 0x8a, 0x86, 0xf8, 0xc7, 0x21, 0xf9, 0x57, + 0xf9, 0x43, 0x03, 0xcc, 0xd6, 0x08, 0x6d, 0xda, 0x75, 0x82, 0xc8, 0x1e, 0xa1, 0xc4, 0xab, 0x13, + 0xb8, 0x0c, 0x26, 0xc4, 0xcf, 0x97, 0x01, 0xae, 0xc7, 0xb3, 0xfd, 0x39, 0x15, 0xf2, 0x89, 0xad, + 0x98, 0x81, 0x12, 0x19, 0xfd, 0x3b, 0x40, 0xae, 0xef, 0xef, 0x00, 0x17, 0xc0, 0x68, 0x90, 0x4c, + 0x49, 0x0b, 0x9c, 0x2b, 0x06, 0xa3, 0x82, 0x2a, 0xb8, 0x3e, 0x0d, 0xc5, 0xe8, 0x27, 0xaf, 0xb8, + 0x3e, 0x0d, 0x91, 0xa0, 0x96, 0xff, 0x61, 0x80, 0x5e, 0xff, 0xe2, 0x03, 0x9b, 0x60, 0x9c, 0x49, + 0xd7, 0x55, 0x68, 0xb7, 0xef, 0x33, 0xb4, 0xd9, 0x40, 0xc8, 0xbb, 0x35, 0xa6, 0xc6, 0x60, 0x3c, + 0xba, 0x75, 0x5c, 0x8d, 0x3c, 0x4b, 0x4d, 0x3d, 0xa7, 0x64, 0x74, 0x57, 0x57, 0x24, 0x0d, 0x69, + 0x2e, 0x3c, 0x2f, 0xe7, 0x73, 0xa9, 0xa1, 0x57, 0x3c, 0x9b, 0xab, 0x5e, 0xfe, 0xe8, 0x76, 0xf1, + 0xd4, 0xc7, 0xb7, 0x8b, 0xa7, 0x3e, 0xb9, 0x5d, 0x3c, 0xf5, 0xbd, 0x76, 0xd1, 0xf8, 0xa8, 0x5d, + 0x34, 0x3e, 0x6e, 0x17, 0x8d, 0x4f, 0xda, 0x45, 0xe3, 0x5f, 0xed, 0xa2, 0xf1, 0x8b, 0x4f, 0x8b, + 0xa7, 0xbe, 0x35, 0xae, 0x5c, 0xfb, 0x5f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x44, 0x57, 0xd1, 0xa5, + 0x78, 0x2b, 0x00, 0x00, } diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/generated.proto b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/generated.proto index ce7dd730a21..a0c23a44f56 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/generated.proto +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/generated.proto @@ -443,6 +443,37 @@ message JSONSchemaProps { optional JSON example = 36; optional bool nullable = 37; + + // x-kubernetes-preserve-unknown-fields stops the API server + // decoding step from pruning fields which are not specified + // in the validation schema. This affects fields recursively, + // but switches back to normal pruning behaviour if nested + // properties or additionalProperties are specified in the schema. + optional bool xKubernetesPreserveUnknownFields = 38; + + // x-kubernetes-embedded-resource defines that the value is an + // embedded Kubernetes runtime.Object, with TypeMeta and + // ObjectMeta. The type must be object. It is allowed to further + // restrict the embedded object. kind, apiVersion and metadata + // are validated automatically. x-kubernetes-preserve-unknown-fields + // is allowed to be true, but does not have to be if the object + // is fully specified (up to kind, apiVersion, metadata). + optional bool xKubernetesEmbeddedResource = 39; + + // x-kubernetes-int-or-string specifies that this value is + // either an integer or a string. If this is true, an empty + // type is allowed and type as child of anyOf is permitted + // if following one of the following patterns: + // + // 1) anyOf: + // - type: integer + // - type: string + // 2) allOf: + // - anyOf: + // - type: integer + // - type: string + // - ... zero or more + optional bool xKubernetesIntOrString = 40; } // JSONSchemaPropsOrArray represents a value that can either be a JSONSchemaProps diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/types.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/types.go index ad2e1347ce1..220a494bce2 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/types.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/types.go @@ -279,6 +279,22 @@ const ( // NamesAccepted means the names chosen for this CustomResourceDefinition do not conflict with others in // the group and are therefore accepted. NamesAccepted CustomResourceDefinitionConditionType = "NamesAccepted" + // NonStructuralSchema means that one or more OpenAPI schema is not structural. + // + // A schema is structural if it specifies types for all values, with the only exceptions of those with + // - x-kubernetes-int-or-string: true — for fields which can be integer or string + // - x-kubernetes-preserve-unknown-fields: true — for raw, unspecified JSON values + // and there is no type, additionalProperties, default, nullable or x-kubernetes-* vendor extenions + // specified under allOf, anyOf, oneOf or not. + // + // Non-structural schemas will not be allowed anymore in v1 API groups. Moreover, new features will not be + // available for non-structural CRDs: + // - pruning + // - defaulting + // - read-only + // - OpenAPI publishing + // - webhook conversion + NonStructuralSchema CustomResourceDefinitionConditionType = "NonStructuralSchema" // Terminating means that the CustomResourceDefinition has been deleted and is cleaning up. Terminating CustomResourceDefinitionConditionType = "Terminating" ) diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/types_jsonschema.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/types_jsonschema.go index 54c0a4ae13f..84f26e600af 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/types_jsonschema.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/types_jsonschema.go @@ -55,6 +55,37 @@ type JSONSchemaProps struct { ExternalDocs *ExternalDocumentation `json:"externalDocs,omitempty" protobuf:"bytes,35,opt,name=externalDocs"` Example *JSON `json:"example,omitempty" protobuf:"bytes,36,opt,name=example"` Nullable bool `json:"nullable,omitempty" protobuf:"bytes,37,opt,name=nullable"` + + // x-kubernetes-preserve-unknown-fields stops the API server + // decoding step from pruning fields which are not specified + // in the validation schema. This affects fields recursively, + // but switches back to normal pruning behaviour if nested + // properties or additionalProperties are specified in the schema. + XPreserveUnknownFields bool `json:"x-kubernetes-preserve-unknown-fields,omitempty" protobuf:"bytes,38,opt,name=xKubernetesPreserveUnknownFields"` + + // x-kubernetes-embedded-resource defines that the value is an + // embedded Kubernetes runtime.Object, with TypeMeta and + // ObjectMeta. The type must be object. It is allowed to further + // restrict the embedded object. kind, apiVersion and metadata + // are validated automatically. x-kubernetes-preserve-unknown-fields + // is allowed to be true, but does not have to be if the object + // is fully specified (up to kind, apiVersion, metadata). + XEmbeddedResource bool `json:"x-kubernetes-embedded-resource,omitempty" protobuf:"bytes,39,opt,name=xKubernetesEmbeddedResource"` + + // x-kubernetes-int-or-string specifies that this value is + // either an integer or a string. If this is true, an empty + // type is allowed and type as child of anyOf is permitted + // if following one of the following patterns: + // + // 1) anyOf: + // - type: integer + // - type: string + // 2) allOf: + // - anyOf: + // - type: integer + // - type: string + // - ... zero or more + XIntOrString bool `json:"x-kubernetes-int-or-string,omitempty" protobuf:"bytes,40,opt,name=xKubernetesIntOrString"` } // JSON represents any valid JSON value. diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/zz_generated.conversion.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/zz_generated.conversion.go index bee60f2e258..00a558a7fc9 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/zz_generated.conversion.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/zz_generated.conversion.go @@ -938,6 +938,9 @@ func autoConvert_v1beta1_JSONSchemaProps_To_apiextensions_JSONSchemaProps(in *JS out.Example = nil } out.Nullable = in.Nullable + out.XPreserveUnknownFields = in.XPreserveUnknownFields + out.XEmbeddedResource = in.XEmbeddedResource + out.XIntOrString = in.XIntOrString return nil } @@ -1120,6 +1123,9 @@ func autoConvert_apiextensions_JSONSchemaProps_To_v1beta1_JSONSchemaProps(in *ap } else { out.Example = nil } + out.XPreserveUnknownFields = in.XPreserveUnknownFields + out.XEmbeddedResource = in.XEmbeddedResource + out.XIntOrString = in.XIntOrString return nil } diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation.go index 6e4c17a7cc9..e91de0eaa88 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation.go @@ -686,6 +686,10 @@ func (v *specStandardValidatorV3) validate(schema *apiextensions.JSONSchemaProps return allErrs } + // + // WARNING: if anything new is allowed below, NewStructural must be adapted to support it. + // + if schema.Default != nil { allErrs = append(allErrs, field.Forbidden(fldPath.Child("default"), "default is not supported")) } @@ -786,7 +790,7 @@ func validateSimpleJSONPath(s string, fldPath *field.Path) field.ErrorList { return allErrs } -var allowedFieldsAtRootSchema = []string{"Description", "Type", "Format", "Title", "Maximum", "ExclusiveMaximum", "Minimum", "ExclusiveMinimum", "MaxLength", "MinLength", "Pattern", "MaxItems", "MinItems", "UniqueItems", "MultipleOf", "Required", "Items", "Properties", "ExternalDocs", "Example"} +var allowedFieldsAtRootSchema = []string{"Description", "Type", "Format", "Title", "Maximum", "ExclusiveMaximum", "Minimum", "ExclusiveMinimum", "MaxLength", "MinLength", "Pattern", "MaxItems", "MinItems", "UniqueItems", "MultipleOf", "Required", "Items", "Properties", "ExternalDocs", "Example", "XPreserveUnknownFields"} func allowedAtRootSchema(field string) bool { for _, v := range allowedFieldsAtRootSchema { diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/BUILD b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/BUILD index c7378143b9d..d624136ac96 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/BUILD +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/BUILD @@ -32,6 +32,7 @@ go_library( "//staging/src/k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion:go_default_library", "//staging/src/k8s.io/apiextensions-apiserver/pkg/controller/establish:go_default_library", "//staging/src/k8s.io/apiextensions-apiserver/pkg/controller/finalizer:go_default_library", + "//staging/src/k8s.io/apiextensions-apiserver/pkg/controller/nonstructuralschema:go_default_library", "//staging/src/k8s.io/apiextensions-apiserver/pkg/controller/openapi:go_default_library", "//staging/src/k8s.io/apiextensions-apiserver/pkg/controller/status:go_default_library", "//staging/src/k8s.io/apiextensions-apiserver/pkg/crdserverscheme:go_default_library", @@ -119,6 +120,7 @@ filegroup( srcs = [ ":package-srcs", "//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/conversion:all-srcs", + "//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema:all-srcs", "//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation:all-srcs", ], tags = ["automanaged"], diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/apiserver.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/apiserver.go index 4084910359c..1a1496ec8f6 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/apiserver.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/apiserver.go @@ -30,6 +30,7 @@ import ( internalinformers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion" "k8s.io/apiextensions-apiserver/pkg/controller/establish" "k8s.io/apiextensions-apiserver/pkg/controller/finalizer" + "k8s.io/apiextensions-apiserver/pkg/controller/nonstructuralschema" openapicontroller "k8s.io/apiextensions-apiserver/pkg/controller/openapi" "k8s.io/apiextensions-apiserver/pkg/controller/status" apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features" @@ -195,6 +196,7 @@ func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget) crdController := NewDiscoveryController(s.Informers.Apiextensions().InternalVersion().CustomResourceDefinitions(), versionDiscoveryHandler, groupDiscoveryHandler) namingController := status.NewNamingConditionController(s.Informers.Apiextensions().InternalVersion().CustomResourceDefinitions(), crdClient.Apiextensions()) + nonStructuralSchemaController := nonstructuralschema.NewConditionController(s.Informers.Apiextensions().InternalVersion().CustomResourceDefinitions(), crdClient.Apiextensions()) finalizingController := finalizer.NewCRDFinalizer( s.Informers.Apiextensions().InternalVersion().CustomResourceDefinitions(), crdClient.Apiextensions(), @@ -217,6 +219,7 @@ func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget) go crdController.Run(context.StopCh) go namingController.Run(context.StopCh) go establishingController.Run(context.StopCh) + go nonStructuralSchemaController.Run(5, context.StopCh) go finalizingController.Run(5, context.StopCh) return nil }) diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/BUILD b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/BUILD new file mode 100644 index 00000000000..2a8cee01698 --- /dev/null +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/BUILD @@ -0,0 +1,44 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = [ + "complete.go", + "convert.go", + "structural.go", + "validation.go", + "zz_generated.deepcopy.go", + ], + importmap = "k8s.io/kubernetes/vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/schema", + importpath = "k8s.io/apiextensions-apiserver/pkg/apiserver/schema", + visibility = ["//visibility:public"], + deps = [ + "//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) + +go_test( + name = "go_default_test", + srcs = ["validation_test.go"], + embed = [":go_default_library"], + deps = [ + "//staging/src/k8s.io/apimachinery/pkg/util/rand:go_default_library", + "//vendor/github.com/google/gofuzz:go_default_library", + ], +) diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/complete.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/complete.go new file mode 100644 index 00000000000..08e222f0d0e --- /dev/null +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/complete.go @@ -0,0 +1,82 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package schema + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/util/validation/field" +) + +// validateStructuralCompleteness checks that every specified field or array in s is also specified +// outside of value validation. +func validateStructuralCompleteness(s *Structural, fldPath *field.Path) field.ErrorList { + if s == nil { + return nil + } + + return validateValueValidationCompleteness(s.ValueValidation, s, fldPath, fldPath) +} + +func validateValueValidationCompleteness(v *ValueValidation, s *Structural, sPath, vPath *field.Path) field.ErrorList { + if v == nil { + return nil + } + if s == nil { + return field.ErrorList{field.Required(sPath, fmt.Sprintf("because it is defined in %s", vPath.String()))} + } + + allErrs := field.ErrorList{} + + allErrs = append(allErrs, validateNestedValueValidationCompleteness(v.Not, s, sPath, vPath.Child("not"))...) + for i := range v.AllOf { + allErrs = append(allErrs, validateNestedValueValidationCompleteness(&v.AllOf[i], s, sPath, vPath.Child("allOf").Index(i))...) + } + for i := range v.AnyOf { + allErrs = append(allErrs, validateNestedValueValidationCompleteness(&v.AnyOf[i], s, sPath, vPath.Child("anyOf").Index(i))...) + } + for i := range v.OneOf { + allErrs = append(allErrs, validateNestedValueValidationCompleteness(&v.OneOf[i], s, sPath, vPath.Child("oneOf").Index(i))...) + } + + return allErrs +} + +func validateNestedValueValidationCompleteness(v *NestedValueValidation, s *Structural, sPath, vPath *field.Path) field.ErrorList { + if v == nil { + return nil + } + if s == nil { + return field.ErrorList{field.Required(sPath, fmt.Sprintf("because it is defined in %s", vPath.String()))} + } + + allErrs := field.ErrorList{} + + allErrs = append(allErrs, validateValueValidationCompleteness(&v.ValueValidation, s, sPath, vPath)...) + allErrs = append(allErrs, validateNestedValueValidationCompleteness(v.Items, s.Items, sPath.Child("items"), vPath.Child("items"))...) + for k, vFld := range v.Properties { + if sFld, ok := s.Properties[k]; !ok { + allErrs = append(allErrs, field.Required(sPath.Child("properties").Key(k), fmt.Sprintf("because it is defined in %s", vPath.Child("properties").Key(k)))) + } else { + allErrs = append(allErrs, validateNestedValueValidationCompleteness(&vFld, &sFld, sPath.Child("properties").Key(k), vPath.Child("properties").Key(k))...) + } + } + + // don't check additionalProperties as this is not allowed (and checked during validation) + + return allErrs +} diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/convert.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/convert.go new file mode 100644 index 00000000000..2ed71b2618c --- /dev/null +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/convert.go @@ -0,0 +1,276 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package schema + +import ( + "fmt" + + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" +) + +// NewStructural converts an OpenAPI v3 schema into a structural schema. A pre-validated JSONSchemaProps will +// not fail on NewStructural. This means that we require that: +// +// - items is not an array of schemas +// - the following fields are not set: +// - id +// - schema +// - $ref +// - patternProperties +// - dependencies +// - additionalItems +// - definitions. +// +// The follow fields are not preserved: +// - externalDocs +// - example. +func NewStructural(s *apiextensions.JSONSchemaProps) (*Structural, error) { + if s == nil { + return nil, nil + } + + if err := validateUnsupportedFields(s); err != nil { + return nil, err + } + + vv, err := newValueValidation(s) + if err != nil { + return nil, err + } + + g, err := newGenerics(s) + if err != nil { + return nil, err + } + + x, err := newExtensions(s) + if err != nil { + return nil, err + } + + ss := &Structural{ + Generic: *g, + Extensions: *x, + ValueValidation: vv, + } + + if s.Items != nil { + if len(s.Items.JSONSchemas) > 0 { + // we validate that it is not an array + return nil, fmt.Errorf("OpenAPIV3Schema 'items' must be a schema, but is an array") + } + item, err := NewStructural(s.Items.Schema) + if err != nil { + return nil, err + } + ss.Items = item + } + + if len(s.Properties) > 0 { + ss.Properties = make(map[string]Structural, len(s.Properties)) + for k, x := range s.Properties { + fld, err := NewStructural(&x) + if err != nil { + return nil, err + } + ss.Properties[k] = *fld + } + } + + return ss, nil +} + +func newGenerics(s *apiextensions.JSONSchemaProps) (*Generic, error) { + if s == nil { + return nil, nil + } + g := &Generic{ + Type: s.Type, + Description: s.Description, + Title: s.Title, + Nullable: s.Nullable, + } + if s.Default != nil { + g.Default = JSON{interface{}(*s.Default)} + } + + if s.AdditionalProperties != nil { + if s.AdditionalProperties.Schema != nil { + ss, err := NewStructural(s.AdditionalProperties.Schema) + if err != nil { + return nil, err + } + g.AdditionalProperties = &StructuralOrBool{Structural: ss} + } else { + g.AdditionalProperties = &StructuralOrBool{Bool: s.AdditionalProperties.Allows} + } + } + + return g, nil +} + +func newValueValidation(s *apiextensions.JSONSchemaProps) (*ValueValidation, error) { + if s == nil { + return nil, nil + } + not, err := newNestedValueValidation(s.Not) + if err != nil { + return nil, err + } + v := &ValueValidation{ + Format: s.Format, + Maximum: s.Maximum, + ExclusiveMaximum: s.ExclusiveMaximum, + Minimum: s.Minimum, + ExclusiveMinimum: s.ExclusiveMinimum, + MaxLength: s.MaxLength, + MinLength: s.MinLength, + Pattern: s.Pattern, + MaxItems: s.MaxItems, + MinItems: s.MinItems, + UniqueItems: s.UniqueItems, + MultipleOf: s.MultipleOf, + MaxProperties: s.MaxProperties, + MinProperties: s.MinProperties, + Required: s.Required, + Not: not, + } + + for _, e := range s.Enum { + v.Enum = append(v.Enum, JSON{e}) + } + + for _, x := range s.AllOf { + clause, err := newNestedValueValidation(&x) + if err != nil { + return nil, err + } + v.AllOf = append(v.AllOf, *clause) + } + + for _, x := range s.AnyOf { + clause, err := newNestedValueValidation(&x) + if err != nil { + return nil, err + } + v.AnyOf = append(v.AnyOf, *clause) + } + + for _, x := range s.OneOf { + clause, err := newNestedValueValidation(&x) + if err != nil { + return nil, err + } + v.OneOf = append(v.OneOf, *clause) + } + + return v, nil +} + +func newNestedValueValidation(s *apiextensions.JSONSchemaProps) (*NestedValueValidation, error) { + if s == nil { + return nil, nil + } + + if err := validateUnsupportedFields(s); err != nil { + return nil, err + } + + vv, err := newValueValidation(s) + if err != nil { + return nil, err + } + + g, err := newGenerics(s) + if err != nil { + return nil, err + } + + x, err := newExtensions(s) + if err != nil { + return nil, err + } + + v := &NestedValueValidation{ + ValueValidation: *vv, + ForbiddenGenerics: *g, + ForbiddenExtensions: *x, + } + + if s.Items != nil { + if len(s.Items.JSONSchemas) > 0 { + // we validate that it is not an array + return nil, fmt.Errorf("OpenAPIV3Schema 'items' must be a schema, but is an array") + } + nvv, err := newNestedValueValidation(s.Items.Schema) + if err != nil { + return nil, err + } + v.Items = nvv + } + if s.Properties != nil { + v.Properties = make(map[string]NestedValueValidation, len(s.Properties)) + for k, x := range s.Properties { + nvv, err := newNestedValueValidation(&x) + if err != nil { + return nil, err + } + v.Properties[k] = *nvv + } + } + + return v, nil +} + +func newExtensions(s *apiextensions.JSONSchemaProps) (*Extensions, error) { + if s == nil { + return nil, nil + } + + return &Extensions{ + XPreserveUnknownFields: s.XPreserveUnknownFields, + XEmbeddedResource: s.XEmbeddedResource, + XIntOrString: s.XIntOrString, + }, nil +} + +// validateUnsupportedFields checks that those fields rejected by validation are actually unset. +func validateUnsupportedFields(s *apiextensions.JSONSchemaProps) error { + if len(s.ID) > 0 { + return fmt.Errorf("OpenAPIV3Schema 'id' is not supported") + } + if len(s.Schema) > 0 { + return fmt.Errorf("OpenAPIV3Schema 'schema' is not supported") + } + if s.Ref != nil && len(*s.Ref) > 0 { + return fmt.Errorf("OpenAPIV3Schema '$ref' is not supported") + } + if len(s.PatternProperties) > 0 { + return fmt.Errorf("OpenAPIV3Schema 'patternProperties' is not supported") + } + if len(s.Dependencies) > 0 { + return fmt.Errorf("OpenAPIV3Schema 'dependencies' is not supported") + } + if s.AdditionalItems != nil { + return fmt.Errorf("OpenAPIV3Schema 'additionalItems' is not supported") + } + if len(s.Definitions) > 0 { + return fmt.Errorf("OpenAPIV3Schema 'definitions' is not supported") + } + + return nil +} diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/structural.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/structural.go new file mode 100644 index 00000000000..996336c7dc7 --- /dev/null +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/structural.go @@ -0,0 +1,160 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package schema + +import ( + "k8s.io/apimachinery/pkg/runtime" +) + +// +k8s:deepcopy-gen=true + +// Structural represents a structural schema. +type Structural struct { + Items *Structural + Properties map[string]Structural + + Generic + Extensions + + *ValueValidation +} + +// +k8s:deepcopy-gen=true + +// StructuralOrBool is either a structural schema or a boolean. +type StructuralOrBool struct { + Structural *Structural + Bool bool +} + +// +k8s:deepcopy-gen=true + +// Generic contains the generic schema fields not allowed in value validation. +type Generic struct { + Description string + // type specifies the type of a value. + // It can be object, array, number, integer, boolean, string. + // It is optional only if x-kubernetes-preserve-unknown-fields + // or x-kubernetes-int-or-string is true. + Type string + Title string + Default JSON + AdditionalProperties *StructuralOrBool + Nullable bool +} + +// +k8s:deepcopy-gen=true + +// Extensions contains the Kubernetes OpenAPI v3 vendor extensions. +type Extensions struct { + // x-kubernetes-preserve-unknown-fields stops the API server + // decoding step from pruning fields which are not specified + // in the validation schema. This affects fields recursively, + // but switches back to normal pruning behaviour if nested + // properties or additionalProperties are specified in the schema. + XPreserveUnknownFields bool + + // x-kubernetes-embedded-resource defines that the value is an + // embedded Kubernetes runtime.Object, with TypeMeta and + // ObjectMeta. The type must be object. It is allowed to further + // restrict the embedded object. Both ObjectMeta and TypeMeta + // are validated automatically. x-kubernetes-preserve-unknown-fields + // must be true. + XEmbeddedResource bool + + // x-kubernetes-int-or-string specifies that this value is + // either an integer or a string. If this is true, an empty + // type is allowed and type as child of anyOf is permitted + // if following one of the following patterns: + // + // 1) anyOf: + // - type: integer + // - type: string + // 2) allOf: + // - anyOf: + // - type: integer + // - type: string + // - ... zero or more + XIntOrString bool +} + +// +k8s:deepcopy-gen=true + +// ValueValidation contains all schema fields not contributing to the structure of the schema. +type ValueValidation struct { + Format string + Maximum *float64 + ExclusiveMaximum bool + Minimum *float64 + ExclusiveMinimum bool + MaxLength *int64 + MinLength *int64 + Pattern string + MaxItems *int64 + MinItems *int64 + UniqueItems bool + MultipleOf *float64 + Enum []JSON + MaxProperties *int64 + MinProperties *int64 + Required []string + AllOf []NestedValueValidation + OneOf []NestedValueValidation + AnyOf []NestedValueValidation + Not *NestedValueValidation +} + +// +k8s:deepcopy-gen=true + +// NestedValueValidation contains value validations, items and properties usable when nested +// under a logical junctor, and catch all structs for generic and vendor extensions schema fields. +type NestedValueValidation struct { + ValueValidation + + Items *NestedValueValidation + Properties map[string]NestedValueValidation + + // Anything set in the following will make the scheme + // non-structural, with the exception of these two patterns if + // x-kubernetes-int-or-string is true: + // + // 1) anyOf: + // - type: integer + // - type: string + // 2) allOf: + // - anyOf: + // - type: integer + // - type: string + // - ... zero or more + ForbiddenGenerics Generic + ForbiddenExtensions Extensions +} + +// JSON wraps an arbitrary JSON value to be able to implement deepcopy. +type JSON struct { + Object interface{} +} + +// DeepCopy creates a deep copy of the wrapped JSON value. +func (j JSON) DeepCopy() JSON { + return JSON{runtime.DeepCopyJSONValue(j.Object)} +} + +// DeepCopyInto creates a deep copy of the wrapped JSON value and stores it in into. +func (j JSON) DeepCopyInto(into *JSON) { + into.Object = runtime.DeepCopyJSONValue(j.Object) +} diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/validation.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/validation.go new file mode 100644 index 00000000000..1e59ee18303 --- /dev/null +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/validation.go @@ -0,0 +1,242 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package schema + +import ( + "reflect" + + "k8s.io/apimachinery/pkg/util/validation/field" +) + +var intOrStringAnyOf = []NestedValueValidation{ + {ForbiddenGenerics: Generic{ + Type: "integer", + }}, + {ForbiddenGenerics: Generic{ + Type: "string", + }}, +} + +type level int + +const ( + rootLevel level = iota + itemLevel + fieldLevel +) + +// ValidateStructural checks that s is a structural schema with the invariants: +// +// * structurality: both `ForbiddenGenerics` and `ForbiddenExtensions` only have zero values, with the two exceptions for IntOrString. +// * RawExtension: for every schema with `x-kubernetes-embedded-resource: true`, `x-kubernetes-preserve-unknown-fields: true` and `type: object` are set +// * IntOrString: for `x-kubernetes-int-or-string: true` either `type` is empty under `anyOf` and `allOf` or the schema structure is one of these: +// +// 1) anyOf: +// - type: integer +// - type: string +// 2) allOf: +// - anyOf: +// - type: integer +// - type: string +// - ... zero or more +// +// * every specified field or array in s is also specified outside of value validation. +// * additionalProperties at the root is not allowed. +func ValidateStructural(s *Structural, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + allErrs = append(allErrs, validateStructuralInvariants(s, rootLevel, fldPath)...) + allErrs = append(allErrs, validateStructuralCompleteness(s, fldPath)...) + + return allErrs +} + +// validateStructuralInvariants checks the invariants of a structural schema. +func validateStructuralInvariants(s *Structural, lvl level, fldPath *field.Path) field.ErrorList { + if s == nil { + return nil + } + + allErrs := field.ErrorList{} + + allErrs = append(allErrs, validateStructuralInvariants(s.Items, itemLevel, fldPath.Child("items"))...) + for k, v := range s.Properties { + allErrs = append(allErrs, validateStructuralInvariants(&v, fieldLevel, fldPath.Child("properties").Key(k))...) + } + allErrs = append(allErrs, validateGeneric(&s.Generic, lvl, fldPath)...) + allErrs = append(allErrs, validateExtensions(&s.Extensions, fldPath)...) + + // detect the two IntOrString exceptions: + // 1) anyOf: + // - type: integer + // - type: string + // 2) allOf: + // - anyOf: + // - type: integer + // - type: string + // - ... zero or more + skipAnyOf := false + skipFirstAllOfAnyOf := false + if s.XIntOrString && s.ValueValidation != nil { + if len(s.ValueValidation.AnyOf) == 2 && reflect.DeepEqual(s.ValueValidation.AnyOf, intOrStringAnyOf) { + skipAnyOf = true + } else if len(s.ValueValidation.AllOf) >= 1 && len(s.ValueValidation.AllOf[0].AnyOf) == 2 && reflect.DeepEqual(s.ValueValidation.AllOf[0].AnyOf, intOrStringAnyOf) { + skipFirstAllOfAnyOf = true + } + } + + allErrs = append(allErrs, validateValueValidation(s.ValueValidation, skipAnyOf, skipFirstAllOfAnyOf, fldPath)...) + + if s.XEmbeddedResource && s.Type != "object" { + if len(s.Type) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("type"), "must be object if x-kubernetes-embedded-resource is true")) + } else { + allErrs = append(allErrs, field.Invalid(fldPath.Child("type"), s.Type, "must be object if x-kubernetes-embedded-resource is true")) + } + } else if len(s.Type) == 0 && !s.Extensions.XIntOrString && !s.Extensions.XPreserveUnknownFields { + switch lvl { + case rootLevel: + allErrs = append(allErrs, field.Required(fldPath.Child("type"), "must not be empty at the root")) + case itemLevel: + allErrs = append(allErrs, field.Required(fldPath.Child("type"), "must not be empty for specified array items")) + case fieldLevel: + allErrs = append(allErrs, field.Required(fldPath.Child("type"), "must not be empty for specified object fields")) + } + } + + if lvl == rootLevel && len(s.Type) > 0 && s.Type != "object" { + allErrs = append(allErrs, field.Invalid(fldPath.Child("type"), s.Type, "must be object at the root")) + } + + if s.XEmbeddedResource && !s.XPreserveUnknownFields && s.Properties == nil { + allErrs = append(allErrs, field.Required(fldPath.Child("properties"), "must not be empty if x-kubernetes-embedded-resource is true without x-kubernetes-preserve-unknown-fields")) + } + + return allErrs +} + +// validateGeneric checks the generic fields of a structural schema. +func validateGeneric(g *Generic, lvl level, fldPath *field.Path) field.ErrorList { + if g == nil { + return nil + } + + allErrs := field.ErrorList{} + + if g.AdditionalProperties != nil { + if lvl == rootLevel { + allErrs = append(allErrs, field.Forbidden(fldPath.Child("additionalProperties"), "must not be used at the root")) + } + if g.AdditionalProperties.Structural != nil { + allErrs = append(allErrs, validateStructuralInvariants(g.AdditionalProperties.Structural, fieldLevel, fldPath.Child("additionalProperties"))...) + } + } + + return allErrs +} + +// validateExtensions checks Kubernetes vendor extensions of a structural schema. +func validateExtensions(x *Extensions, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + if x.XIntOrString && x.XPreserveUnknownFields { + allErrs = append(allErrs, field.Invalid(fldPath.Child("x-kubernetes-preserve-unknown-fields"), x.XPreserveUnknownFields, "must be false if x-kubernetes-int-or-string is true")) + } + if x.XIntOrString && x.XEmbeddedResource { + allErrs = append(allErrs, field.Invalid(fldPath.Child("x-kubernetes-embedded-resource"), x.XEmbeddedResource, "must be false if x-kubernetes-int-or-string is true")) + } + + return allErrs +} + +// validateValueValidation checks the value validation in a structural schema. +func validateValueValidation(v *ValueValidation, skipAnyOf, skipFirstAllOfAnyOf bool, fldPath *field.Path) field.ErrorList { + if v == nil { + return nil + } + + allErrs := field.ErrorList{} + + if !skipAnyOf { + for i := range v.AnyOf { + allErrs = append(allErrs, validateNestedValueValidation(&v.AnyOf[i], false, false, fldPath.Child("anyOf").Index(i))...) + } + } + + for i := range v.AllOf { + skipAnyOf := false + if skipFirstAllOfAnyOf && i == 0 { + skipAnyOf = true + } + allErrs = append(allErrs, validateNestedValueValidation(&v.AllOf[i], skipAnyOf, false, fldPath.Child("allOf").Index(i))...) + } + + for i := range v.OneOf { + allErrs = append(allErrs, validateNestedValueValidation(&v.OneOf[i], false, false, fldPath.Child("oneOf").Index(i))...) + } + + allErrs = append(allErrs, validateNestedValueValidation(v.Not, false, false, fldPath.Child("not"))...) + + return allErrs +} + +// validateNestedValueValidation checks the nested value validation under a logic junctor in a structural schema. +func validateNestedValueValidation(v *NestedValueValidation, skipAnyOf, skipAllOfAnyOf bool, fldPath *field.Path) field.ErrorList { + if v == nil { + return nil + } + + allErrs := field.ErrorList{} + + allErrs = append(allErrs, validateValueValidation(&v.ValueValidation, skipAnyOf, skipAllOfAnyOf, fldPath)...) + allErrs = append(allErrs, validateNestedValueValidation(v.Items, false, false, fldPath.Child("items"))...) + + for k, fld := range v.Properties { + allErrs = append(allErrs, validateNestedValueValidation(&fld, false, false, fldPath.Child("properties").Key(k))...) + } + + if len(v.ForbiddenGenerics.Type) > 0 { + allErrs = append(allErrs, field.Forbidden(fldPath.Child("type"), "must be empty to be structural")) + } + if v.ForbiddenGenerics.AdditionalProperties != nil { + allErrs = append(allErrs, field.Forbidden(fldPath.Child("additionalProperties"), "must be undefined to be structural")) + } + if v.ForbiddenGenerics.Default.Object != nil { + allErrs = append(allErrs, field.Forbidden(fldPath.Child("default"), "must be undefined to be structural")) + } + if len(v.ForbiddenGenerics.Title) > 0 { + allErrs = append(allErrs, field.Forbidden(fldPath.Child("title"), "must be empty to be structural")) + } + if len(v.ForbiddenGenerics.Description) > 0 { + allErrs = append(allErrs, field.Forbidden(fldPath.Child("description"), "must be empty to be structural")) + } + if v.ForbiddenGenerics.Nullable { + allErrs = append(allErrs, field.Forbidden(fldPath.Child("nullable"), "must be false to be structural")) + } + + if v.ForbiddenExtensions.XPreserveUnknownFields { + allErrs = append(allErrs, field.Forbidden(fldPath.Child("x-kubernetes-preserve-unknown-fields"), "must be false to be structural")) + } + if v.ForbiddenExtensions.XEmbeddedResource { + allErrs = append(allErrs, field.Forbidden(fldPath.Child("x-kubernetes-embedded-resource"), "must be false to be structural")) + } + if v.ForbiddenExtensions.XIntOrString { + allErrs = append(allErrs, field.Forbidden(fldPath.Child("x-kubernetes-int-or-string"), "must be false to be structural")) + } + + return allErrs +} diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/validation_test.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/validation_test.go new file mode 100644 index 00000000000..619040771a1 --- /dev/null +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/validation_test.go @@ -0,0 +1,71 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package schema + +import ( + "reflect" + "testing" + + fuzz "github.com/google/gofuzz" + + "k8s.io/apimachinery/pkg/util/rand" +) + +func TestValidateNestedValueValidationComplete(t *testing.T) { + fuzzer := fuzz.New() + fuzzer.Funcs( + func(s *JSON, c fuzz.Continue) { + if c.RandBool() { + s.Object = float64(42.0) + } + }, + func(s **StructuralOrBool, c fuzz.Continue) { + if c.RandBool() { + *s = &StructuralOrBool{} + } + }, + ) + fuzzer.NilChance(0) + + // check that we didn't forget to check any forbidden generic field + tt := reflect.TypeOf(Generic{}) + for i := 0; i < tt.NumField(); i++ { + vv := &NestedValueValidation{} + x := reflect.ValueOf(&vv.ForbiddenGenerics).Elem() + i := rand.Intn(x.NumField()) + fuzzer.Fuzz(x.Field(i).Addr().Interface()) + + errs := validateNestedValueValidation(vv, false, false, nil) + if len(errs) == 0 && !reflect.DeepEqual(vv.ForbiddenGenerics, Generic{}) { + t.Errorf("expected ForbiddenGenerics validation errors for: %#v", vv) + } + } + + // check that we didn't forget to check any forbidden extension field + tt = reflect.TypeOf(Extensions{}) + for i := 0; i < tt.NumField(); i++ { + vv := &NestedValueValidation{} + x := reflect.ValueOf(&vv.ForbiddenExtensions).Elem() + i := rand.Intn(x.NumField()) + fuzzer.Fuzz(x.Field(i).Addr().Interface()) + + errs := validateNestedValueValidation(vv, false, false, nil) + if len(errs) == 0 && !reflect.DeepEqual(vv.ForbiddenExtensions, Extensions{}) { + t.Errorf("expected ForbiddenExtensions validation errors for: %#v", vv) + } + } +} diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/zz_generated.deepcopy.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/zz_generated.deepcopy.go new file mode 100644 index 00000000000..01b566c8789 --- /dev/null +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/zz_generated.deepcopy.go @@ -0,0 +1,245 @@ +// +build !ignore_autogenerated + +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package schema + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Extensions) DeepCopyInto(out *Extensions) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Extensions. +func (in *Extensions) DeepCopy() *Extensions { + if in == nil { + return nil + } + out := new(Extensions) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Generic) DeepCopyInto(out *Generic) { + *out = *in + out.Default = in.Default.DeepCopy() + if in.AdditionalProperties != nil { + in, out := &in.AdditionalProperties, &out.AdditionalProperties + *out = new(StructuralOrBool) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Generic. +func (in *Generic) DeepCopy() *Generic { + if in == nil { + return nil + } + out := new(Generic) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NestedValueValidation) DeepCopyInto(out *NestedValueValidation) { + *out = *in + in.ValueValidation.DeepCopyInto(&out.ValueValidation) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = new(NestedValueValidation) + (*in).DeepCopyInto(*out) + } + if in.Properties != nil { + in, out := &in.Properties, &out.Properties + *out = make(map[string]NestedValueValidation, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } + in.ForbiddenGenerics.DeepCopyInto(&out.ForbiddenGenerics) + out.ForbiddenExtensions = in.ForbiddenExtensions + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NestedValueValidation. +func (in *NestedValueValidation) DeepCopy() *NestedValueValidation { + if in == nil { + return nil + } + out := new(NestedValueValidation) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Structural) DeepCopyInto(out *Structural) { + *out = *in + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = new(Structural) + (*in).DeepCopyInto(*out) + } + if in.Properties != nil { + in, out := &in.Properties, &out.Properties + *out = make(map[string]Structural, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } + in.Generic.DeepCopyInto(&out.Generic) + out.Extensions = in.Extensions + if in.ValueValidation != nil { + in, out := &in.ValueValidation, &out.ValueValidation + *out = new(ValueValidation) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Structural. +func (in *Structural) DeepCopy() *Structural { + if in == nil { + return nil + } + out := new(Structural) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *StructuralOrBool) DeepCopyInto(out *StructuralOrBool) { + *out = *in + if in.Structural != nil { + in, out := &in.Structural, &out.Structural + *out = new(Structural) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StructuralOrBool. +func (in *StructuralOrBool) DeepCopy() *StructuralOrBool { + if in == nil { + return nil + } + out := new(StructuralOrBool) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ValueValidation) DeepCopyInto(out *ValueValidation) { + *out = *in + if in.Maximum != nil { + in, out := &in.Maximum, &out.Maximum + *out = new(float64) + **out = **in + } + if in.Minimum != nil { + in, out := &in.Minimum, &out.Minimum + *out = new(float64) + **out = **in + } + if in.MaxLength != nil { + in, out := &in.MaxLength, &out.MaxLength + *out = new(int64) + **out = **in + } + if in.MinLength != nil { + in, out := &in.MinLength, &out.MinLength + *out = new(int64) + **out = **in + } + if in.MaxItems != nil { + in, out := &in.MaxItems, &out.MaxItems + *out = new(int64) + **out = **in + } + if in.MinItems != nil { + in, out := &in.MinItems, &out.MinItems + *out = new(int64) + **out = **in + } + if in.MultipleOf != nil { + in, out := &in.MultipleOf, &out.MultipleOf + *out = new(float64) + **out = **in + } + if in.Enum != nil { + in, out := &in.Enum, &out.Enum + *out = make([]JSON, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.MaxProperties != nil { + in, out := &in.MaxProperties, &out.MaxProperties + *out = new(int64) + **out = **in + } + if in.MinProperties != nil { + in, out := &in.MinProperties, &out.MinProperties + *out = new(int64) + **out = **in + } + if in.Required != nil { + in, out := &in.Required, &out.Required + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.AllOf != nil { + in, out := &in.AllOf, &out.AllOf + *out = make([]NestedValueValidation, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.OneOf != nil { + in, out := &in.OneOf, &out.OneOf + *out = make([]NestedValueValidation, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.AnyOf != nil { + in, out := &in.AnyOf, &out.AnyOf + *out = make([]NestedValueValidation, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Not != nil { + in, out := &in.Not, &out.Not + *out = new(NestedValueValidation) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ValueValidation. +func (in *ValueValidation) DeepCopy() *ValueValidation { + if in == nil { + return nil + } + out := new(ValueValidation) + in.DeepCopyInto(out) + return out +} diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/validation.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/validation.go index 8e2b4ee9ea0..6557d88317d 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/validation.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/validation/validation.go @@ -194,6 +194,16 @@ func ConvertJSONSchemaPropsWithPostProcess(in *apiextensions.JSONSchemaProps, ou } } + if in.XPreserveUnknownFields { + out.VendorExtensible.AddExtension("x-kubernetes-preserve-unknown-fields", true) + } + if in.XEmbeddedResource { + out.VendorExtensible.AddExtension("x-kubernetes-embedded-resource", true) + } + if in.XIntOrString { + out.VendorExtensible.AddExtension("x-kubernetes-int-or-string", true) + } + return nil } diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/controller/nonstructuralschema/BUILD b/staging/src/k8s.io/apiextensions-apiserver/pkg/controller/nonstructuralschema/BUILD new file mode 100644 index 00000000000..e6fbd704bca --- /dev/null +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/controller/nonstructuralschema/BUILD @@ -0,0 +1,38 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["nonstructuralschema_controller.go"], + importmap = "k8s.io/kubernetes/vendor/k8s.io/apiextensions-apiserver/pkg/controller/nonstructuralschema", + importpath = "k8s.io/apiextensions-apiserver/pkg/controller/nonstructuralschema", + visibility = ["//visibility:public"], + deps = [ + "//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library", + "//staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema:go_default_library", + "//staging/src/k8s.io/apiextensions-apiserver/pkg/client/clientset/internalclientset/typed/apiextensions/internalversion:go_default_library", + "//staging/src/k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/apiextensions/internalversion:go_default_library", + "//staging/src/k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/runtime:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/validation/field:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library", + "//staging/src/k8s.io/client-go/tools/cache:go_default_library", + "//staging/src/k8s.io/client-go/util/workqueue:go_default_library", + "//vendor/k8s.io/klog:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/controller/nonstructuralschema/nonstructuralschema_controller.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/controller/nonstructuralschema/nonstructuralschema_controller.go new file mode 100644 index 00000000000..1986cedd2cc --- /dev/null +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/controller/nonstructuralschema/nonstructuralschema_controller.go @@ -0,0 +1,236 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package nonstructuralschema + +import ( + "fmt" + "sort" + "time" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/util/workqueue" + "k8s.io/klog" + + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions" + "k8s.io/apiextensions-apiserver/pkg/apiserver/schema" + client "k8s.io/apiextensions-apiserver/pkg/client/clientset/internalclientset/typed/apiextensions/internalversion" + informers "k8s.io/apiextensions-apiserver/pkg/client/informers/internalversion/apiextensions/internalversion" + listers "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion" +) + +// ConditionController is maintaining the NonStructuralSchema condition. +type ConditionController struct { + crdClient client.CustomResourceDefinitionsGetter + + crdLister listers.CustomResourceDefinitionLister + crdSynced cache.InformerSynced + + // To allow injection for testing. + syncFn func(key string) error + + queue workqueue.RateLimitingInterface +} + +// NewConditionController constructs a non-structural schema condition controller. +func NewConditionController( + crdInformer informers.CustomResourceDefinitionInformer, + crdClient client.CustomResourceDefinitionsGetter, +) *ConditionController { + c := &ConditionController{ + crdClient: crdClient, + crdLister: crdInformer.Lister(), + crdSynced: crdInformer.Informer().HasSynced, + queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "non_structural_schema_condition_controller"), + } + + crdInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: c.addCustomResourceDefinition, + UpdateFunc: c.updateCustomResourceDefinition, + DeleteFunc: nil, + }) + + c.syncFn = c.sync + + return c +} + +func calculateCondition(in *apiextensions.CustomResourceDefinition) *apiextensions.CustomResourceDefinitionCondition { + cond := &apiextensions.CustomResourceDefinitionCondition{ + Type: apiextensions.NonStructuralSchema, + Status: apiextensions.ConditionUnknown, + } + + allErrs := field.ErrorList{} + + if in.Spec.Validation != nil && in.Spec.Validation.OpenAPIV3Schema != nil { + s, err := schema.NewStructural(in.Spec.Validation.OpenAPIV3Schema) + if err != nil { + cond.Reason = "StructuralError" + cond.Message = fmt.Sprintf("failed to check global validation schema: %v", err) + return cond + } + + pth := field.NewPath("spec", "validation", "openAPIV3Schema") + + allErrs = append(allErrs, schema.ValidateStructural(s, pth)...) + } + + for _, v := range in.Spec.Versions { + if v.Schema == nil || v.Schema.OpenAPIV3Schema == nil { + continue + } + + s, err := schema.NewStructural(v.Schema.OpenAPIV3Schema) + if err != nil { + cond.Reason = "StructuralError" + cond.Message = fmt.Sprintf("failed to check validation schema for version %s: %v", v.Name, err) + return cond + } + + pth := field.NewPath("spec", "version").Key(v.Name).Child("schema", "openAPIV3Schema") + + allErrs = append(allErrs, schema.ValidateStructural(s, pth)...) + } + + if len(allErrs) == 0 { + return nil + } + + // sort error messages. Otherwise, the condition message will change every sync due to + // randomized map iteration. + sort.Slice(allErrs, func(i, j int) bool { + return allErrs[i].Error() < allErrs[j].Error() + }) + + cond.Status = apiextensions.ConditionTrue + cond.Reason = "Violations" + cond.Message = allErrs.ToAggregate().Error() + + return cond +} + +func (c *ConditionController) sync(key string) error { + inCustomResourceDefinition, err := c.crdLister.Get(key) + if apierrors.IsNotFound(err) { + return nil + } + if err != nil { + return err + } + + // check old condition + cond := calculateCondition(inCustomResourceDefinition) + old := apiextensions.FindCRDCondition(inCustomResourceDefinition, apiextensions.NonStructuralSchema) + + if cond == nil && old == nil { + return nil + } + if cond != nil && old != nil && old.Status == cond.Status && old.Reason == cond.Reason && old.Message == cond.Message { + return nil + } + + // update condition + crd := inCustomResourceDefinition.DeepCopy() + if cond == nil { + apiextensions.RemoveCRDCondition(crd, apiextensions.NonStructuralSchema) + } else { + cond.LastTransitionTime = metav1.NewTime(time.Now()) + apiextensions.SetCRDCondition(crd, *cond) + } + + _, err = c.crdClient.CustomResourceDefinitions().UpdateStatus(crd) + if apierrors.IsNotFound(err) || apierrors.IsConflict(err) { + // deleted or changed in the meantime, we'll get called again + return nil + } + if err != nil { + return err + } + + return nil +} + +// Run starts the controller. +func (c *ConditionController) Run(threadiness int, stopCh <-chan struct{}) { + defer utilruntime.HandleCrash() + defer c.queue.ShutDown() + + klog.Infof("Starting NonStructuralSchemaConditionController") + defer klog.Infof("Shutting down NonStructuralSchemaConditionController") + + if !cache.WaitForCacheSync(stopCh, c.crdSynced) { + return + } + + for i := 0; i < threadiness; i++ { + go wait.Until(c.runWorker, time.Second, stopCh) + } + + <-stopCh +} + +func (c *ConditionController) runWorker() { + for c.processNextWorkItem() { + } +} + +// processNextWorkItem deals with one key off the queue. It returns false when it's time to quit. +func (c *ConditionController) processNextWorkItem() bool { + key, quit := c.queue.Get() + if quit { + return false + } + defer c.queue.Done(key) + + err := c.syncFn(key.(string)) + if err == nil { + c.queue.Forget(key) + return true + } + + utilruntime.HandleError(fmt.Errorf("%v failed with: %v", key, err)) + c.queue.AddRateLimited(key) + + return true +} + +func (c *ConditionController) enqueue(obj *apiextensions.CustomResourceDefinition) { + key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj) + if err != nil { + utilruntime.HandleError(fmt.Errorf("Couldn't get key for object %#v: %v", obj, err)) + return + } + + c.queue.Add(key) +} + +func (c *ConditionController) addCustomResourceDefinition(obj interface{}) { + castObj := obj.(*apiextensions.CustomResourceDefinition) + klog.V(4).Infof("Adding %s", castObj.Name) + c.enqueue(castObj) +} + +func (c *ConditionController) updateCustomResourceDefinition(obj, _ interface{}) { + castObj := obj.(*apiextensions.CustomResourceDefinition) + klog.V(4).Infof("Updating %s", castObj.Name) + c.enqueue(castObj) +} diff --git a/staging/src/k8s.io/apiextensions-apiserver/test/integration/BUILD b/staging/src/k8s.io/apiextensions-apiserver/test/integration/BUILD index b9d951758cc..546aa4920fe 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/test/integration/BUILD +++ b/staging/src/k8s.io/apiextensions-apiserver/test/integration/BUILD @@ -26,6 +26,7 @@ go_test( "//staging/src/k8s.io/api/autoscaling/v1:go_default_library", "//staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1:go_default_library", "//staging/src/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset:go_default_library", + "//staging/src/k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme:go_default_library", "//staging/src/k8s.io/apiextensions-apiserver/pkg/cmd/server/options:go_default_library", "//staging/src/k8s.io/apiextensions-apiserver/pkg/features:go_default_library", "//staging/src/k8s.io/apiextensions-apiserver/test/integration/fixtures:go_default_library", @@ -40,6 +41,7 @@ go_test( "//staging/src/k8s.io/apimachinery/pkg/types:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/json:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/wait:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/yaml:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/watch:go_default_library", "//staging/src/k8s.io/apiserver/pkg/endpoints/request:go_default_library", "//staging/src/k8s.io/apiserver/pkg/features:go_default_library", diff --git a/staging/src/k8s.io/apiextensions-apiserver/test/integration/validation_test.go b/staging/src/k8s.io/apiextensions-apiserver/test/integration/validation_test.go index 0874a1ee572..59b0f22870f 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/test/integration/validation_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/test/integration/validation_test.go @@ -22,10 +22,12 @@ import ( "testing" "time" + clientschema "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/apimachinery/pkg/util/yaml" utilfeature "k8s.io/apiserver/pkg/util/feature" featuregatetesting "k8s.io/component-base/featuregate/testing" @@ -539,6 +541,802 @@ func TestForbiddenFieldsInSchema(t *testing.T) { } } +func TestNonStructuralSchemaConditionUpdate(t *testing.T) { + defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)() + + tearDown, apiExtensionClient, _, err := fixtures.StartDefaultServerWithClients(t) + if err != nil { + t.Fatal(err) + } + defer tearDown() + + manifest := ` +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: foos.tests.apiextensions.k8s.io +spec: + group: tests.apiextensions.k8s.io + version: v1beta1 + names: + plural: foos + singular: foo + kind: Foo + listKind: Foolist + scope: Namespaced + validation: + openAPIV3Schema: + type: object + properties: + a: {} + versions: + - name: v1beta1 + served: true + storage: true +` + + // decode CRD manifest + obj, _, err := clientschema.Codecs.UniversalDeserializer().Decode([]byte(manifest), nil, nil) + if err != nil { + t.Fatalf("failed decoding of: %v\n\n%s", err, manifest) + } + crd := obj.(*apiextensionsv1beta1.CustomResourceDefinition) + name := crd.Name + + // save schema for later + origSchema := crd.Spec.Validation.OpenAPIV3Schema + + // create CRDs + t.Logf("Creating CRD %s", crd.Name) + crd, err = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Create(crd) + if err != nil { + t.Fatalf("unexpected create error: %v", err) + } + + // wait for condition with violations + t.Log("Waiting for NonStructuralSchema condition") + var cond *apiextensionsv1beta1.CustomResourceDefinitionCondition + err = wait.PollImmediate(100*time.Millisecond, 5*time.Second, func() (bool, error) { + obj, err := apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get(name, metav1.GetOptions{}) + if err != nil { + return false, err + } + cond = findCRDCondition(obj, apiextensionsv1beta1.NonStructuralSchema) + return cond != nil, nil + }) + if err != nil { + t.Fatalf("unexpected error waiting for NonStructuralSchema condition: %v", cond) + } + if v := "spec.validation.openAPIV3Schema.properties[a].type: Required value: must not be empty for specified object fields"; !strings.Contains(cond.Message, v) { + t.Fatalf("expected violation %q, but got: %v", v, cond.Message) + } + + // remove schema + t.Log("Remove schema") + for retry := 0; retry < 5; retry++ { + crd, err = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get(name, metav1.GetOptions{}) + if err != nil { + t.Fatalf("unexpected get error: %v", err) + } + crd.Spec.Validation = nil + crd, err = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Update(crd) + if apierrors.IsConflict(err) { + continue + } + if err != nil { + t.Fatalf("unexpected update error: %v", err) + } + } + if err != nil { + t.Fatalf("unexpected update error: %v", err) + } + + // wait for condition to go away + t.Log("Wait for condition to disappear") + err = wait.PollImmediate(100*time.Millisecond, 5*time.Second, func() (bool, error) { + obj, err := apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get(name, metav1.GetOptions{}) + if err != nil { + return false, err + } + cond = findCRDCondition(obj, apiextensionsv1beta1.NonStructuralSchema) + return cond == nil, nil + }) + if err != nil { + t.Fatalf("unexpected error waiting for NonStructuralSchema condition: %v", cond) + } + + // readd schema + t.Log("Readd schema") + for retry := 0; retry < 5; retry++ { + crd, err = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get(name, metav1.GetOptions{}) + if err != nil { + t.Fatalf("unexpected get error: %v", err) + } + crd.Spec.Validation = &apiextensionsv1beta1.CustomResourceValidation{OpenAPIV3Schema: origSchema} + crd, err = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Update(crd) + if apierrors.IsConflict(err) { + continue + } + if err != nil { + t.Fatalf("unexpected update error: %v", err) + } + } + if err != nil { + t.Fatalf("unexpected update error: %v", err) + } + + // wait for condition with violations + t.Log("Wait for condition to reappear") + err = wait.PollImmediate(100*time.Millisecond, 5*time.Second, func() (bool, error) { + obj, err := apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get(name, metav1.GetOptions{}) + if err != nil { + return false, err + } + cond = findCRDCondition(obj, apiextensionsv1beta1.NonStructuralSchema) + return cond != nil, nil + }) + if err != nil { + t.Fatalf("unexpected error waiting for NonStructuralSchema condition: %v", cond) + } + if v := "spec.validation.openAPIV3Schema.properties[a].type: Required value: must not be empty for specified object fields"; !strings.Contains(cond.Message, v) { + t.Fatalf("expected violation %q, but got: %v", v, cond.Message) + } +} + +func TestNonStructuralSchemaCondition(t *testing.T) { + defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, apiextensionsfeatures.CustomResourceWebhookConversion, true)() + + tearDown, apiExtensionClient, _, err := fixtures.StartDefaultServerWithClients(t) + if err != nil { + t.Fatal(err) + } + defer tearDown() + + tmpl := ` +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +spec: + version: v1beta1 + names: + plural: foos + singular: foo + kind: Foo + listKind: Foolist + scope: Namespaced + validation: GLOBAL_SCHEMA + versions: + - name: v1beta1 + served: true + storage: true + schema: V1BETA1_SCHEMA + - name: v1 + served: true + schema: V1_SCHEMA +` + + type Test struct { + desc string + globalSchema, v1Schema, v1beta1Schema string + expectedCreateError bool + expectedViolations []string + unexpectedViolations []string + } + tests := []Test{ + {"empty", "", "", "", false, nil, nil}, + { + desc: "int-or-string and preserve-unknown-fields true", + globalSchema: ` +x-kubernetes-preserve-unknown-fields: true +x-kubernetes-int-or-string: true +`, + expectedViolations: []string{ + "spec.validation.openAPIV3Schema.x-kubernetes-preserve-unknown-fields: Invalid value: true: must be false if x-kubernetes-int-or-string is true", + }, + }, + { + desc: "int-or-string and embedded-resource true", + globalSchema: ` +type: object +x-kubernetes-embedded-resource: true +x-kubernetes-int-or-string: true +`, + expectedViolations: []string{ + "spec.validation.openAPIV3Schema.x-kubernetes-embedded-resource: Invalid value: true: must be false if x-kubernetes-int-or-string is true", + }, + }, + { + desc: "embedded-resource without preserve-unknown-fields", + globalSchema: ` +type: object +x-kubernetes-embedded-resource: true +`, + expectedViolations: []string{ + "spec.validation.openAPIV3Schema.properties: Required value: must not be empty if x-kubernetes-embedded-resource is true without x-kubernetes-preserve-unknown-fields", + }, + }, + { + desc: "embedded-resource without preserve-unknown-fields, but properties", + globalSchema: ` +type: object +x-kubernetes-embedded-resource: true +properties: + apiVersion: + type: string + kind: + type: string + metadata: + type: object +`, + expectedViolations: []string{}, + }, + { + desc: "embedded-resource with preserve-unknown-fields", + globalSchema: ` +type: object +x-kubernetes-embedded-resource: true +x-kubernetes-preserve-unknown-fields: true +`, + expectedViolations: []string{}, + }, + { + desc: "embedded-resource with wrong type", + globalSchema: ` +type: array +x-kubernetes-embedded-resource: true +x-kubernetes-preserve-unknown-fields: true +`, + expectedViolations: []string{ + "spec.validation.openAPIV3Schema.type: Invalid value: \"array\": must be object if x-kubernetes-embedded-resource is true", + }, + }, + { + desc: "embedded-resource with empty type", + globalSchema: ` +type: "" +x-kubernetes-embedded-resource: true +x-kubernetes-preserve-unknown-fields: true +`, + expectedViolations: []string{ + "spec.validation.openAPIV3Schema.type: Required value: must be object if x-kubernetes-embedded-resource is true", + }, + }, + { + desc: "no top-level type", + globalSchema: ` +type: "" +`, + expectedViolations: []string{ + "spec.validation.openAPIV3Schema.type: Required value: must not be empty at the root", + }, + }, + { + desc: "non-object top-level type", + globalSchema: ` +type: "integer" +`, + expectedViolations: []string{ + "spec.validation.openAPIV3Schema.type: Invalid value: \"integer\": must be object at the root", + }, + }, + { + desc: "forbidden in nested value validation", + globalSchema: ` +type: object +properties: + foo: + type: string +not: + type: string + additionalProperties: true + title: hello + description: world + nullable: true +allOf: +- properties: + foo: + type: string + additionalProperties: true + title: hello + description: world + nullable: true +anyOf: +- items: + type: string + additionalProperties: true + title: hello + description: world + nullable: true +oneOf: +- properties: + foo: + type: string + additionalProperties: true + title: hello + description: world + nullable: true +`, + expectedViolations: []string{ + "spec.validation.openAPIV3Schema.anyOf[0].items.type: Forbidden: must be empty to be structural", + "spec.validation.openAPIV3Schema.anyOf[0].items.additionalProperties: Forbidden: must be undefined to be structural", + "spec.validation.openAPIV3Schema.anyOf[0].items.title: Forbidden: must be empty to be structural", + "spec.validation.openAPIV3Schema.anyOf[0].items.description: Forbidden: must be empty to be structural", + "spec.validation.openAPIV3Schema.anyOf[0].items.nullable: Forbidden: must be false to be structural", + "spec.validation.openAPIV3Schema.allOf[0].properties[foo].type: Forbidden: must be empty to be structural", + "spec.validation.openAPIV3Schema.allOf[0].properties[foo].additionalProperties: Forbidden: must be undefined to be structural", + "spec.validation.openAPIV3Schema.allOf[0].properties[foo].title: Forbidden: must be empty to be structural", + "spec.validation.openAPIV3Schema.allOf[0].properties[foo].description: Forbidden: must be empty to be structural", + "spec.validation.openAPIV3Schema.allOf[0].properties[foo].nullable: Forbidden: must be false to be structural", + "spec.validation.openAPIV3Schema.oneOf[0].properties[foo].type: Forbidden: must be empty to be structural", + "spec.validation.openAPIV3Schema.oneOf[0].properties[foo].additionalProperties: Forbidden: must be undefined to be structural", + "spec.validation.openAPIV3Schema.oneOf[0].properties[foo].title: Forbidden: must be empty to be structural", + "spec.validation.openAPIV3Schema.oneOf[0].properties[foo].description: Forbidden: must be empty to be structural", + "spec.validation.openAPIV3Schema.oneOf[0].properties[foo].nullable: Forbidden: must be false to be structural", + "spec.validation.openAPIV3Schema.not.type: Forbidden: must be empty to be structural", + "spec.validation.openAPIV3Schema.not.additionalProperties: Forbidden: must be undefined to be structural", + "spec.validation.openAPIV3Schema.not.title: Forbidden: must be empty to be structural", + "spec.validation.openAPIV3Schema.not.description: Forbidden: must be empty to be structural", + "spec.validation.openAPIV3Schema.not.nullable: Forbidden: must be false to be structural", + "spec.validation.openAPIV3Schema.items: Required value: because it is defined in spec.validation.openAPIV3Schema.anyOf[0].items", + }, + unexpectedViolations: []string{ + "spec.validation.openAPIV3Schema.not.default", + }, + }, + { + desc: "forbidden vendor extensions in nested value validation", + globalSchema: ` +type: object +properties: + int-or-string: + x-kubernetes-int-or-string: true + embedded-resource: + type: object + x-kubernetes-embedded-resource: true + x-kubernetes-preserve-unknown-fields: true +not: + properties: + int-or-string: + x-kubernetes-int-or-string: true + embedded-resource: + x-kubernetes-embedded-resource: true + x-kubernetes-preserve-unknown-fields: true +allOf: +- properties: + int-or-string: + x-kubernetes-int-or-string: true + embedded-resource: + x-kubernetes-embedded-resource: true + x-kubernetes-preserve-unknown-fields: true +anyOf: +- properties: + int-or-string: + x-kubernetes-int-or-string: true + embedded-resource: + x-kubernetes-embedded-resource: true + x-kubernetes-preserve-unknown-fields: true +oneOf: +- properties: + int-or-string: + x-kubernetes-int-or-string: true + embedded-resource: + x-kubernetes-embedded-resource: true + x-kubernetes-preserve-unknown-fields: true +`, + expectedViolations: []string{ + "spec.validation.openAPIV3Schema.allOf[0].properties[embedded-resource].x-kubernetes-preserve-unknown-fields: Forbidden: must be false to be structural", + "spec.validation.openAPIV3Schema.allOf[0].properties[embedded-resource].x-kubernetes-embedded-resource: Forbidden: must be false to be structural", + "spec.validation.openAPIV3Schema.allOf[0].properties[int-or-string].x-kubernetes-int-or-string: Forbidden: must be false to be structural", + "spec.validation.openAPIV3Schema.anyOf[0].properties[embedded-resource].x-kubernetes-preserve-unknown-fields: Forbidden: must be false to be structural", + "spec.validation.openAPIV3Schema.anyOf[0].properties[embedded-resource].x-kubernetes-embedded-resource: Forbidden: must be false to be structural", + "spec.validation.openAPIV3Schema.anyOf[0].properties[int-or-string].x-kubernetes-int-or-string: Forbidden: must be false to be structural", + "spec.validation.openAPIV3Schema.oneOf[0].properties[embedded-resource].x-kubernetes-preserve-unknown-fields: Forbidden: must be false to be structural", + "spec.validation.openAPIV3Schema.oneOf[0].properties[embedded-resource].x-kubernetes-embedded-resource: Forbidden: must be false to be structural", + "spec.validation.openAPIV3Schema.oneOf[0].properties[int-or-string].x-kubernetes-int-or-string: Forbidden: must be false to be structural", + "spec.validation.openAPIV3Schema.not.properties[embedded-resource].x-kubernetes-preserve-unknown-fields: Forbidden: must be false to be structural", + "spec.validation.openAPIV3Schema.not.properties[embedded-resource].x-kubernetes-embedded-resource: Forbidden: must be false to be structural", + "spec.validation.openAPIV3Schema.not.properties[int-or-string].x-kubernetes-int-or-string: Forbidden: must be false to be structural", + }, + }, + { + desc: "missing types", + globalSchema: ` +properties: + foo: + properties: + a: {} + bar: + items: + additionalProperties: + properties: + a: {} + items: {} + abc: + additionalProperties: + properties: + a: + items: + additionalProperties: + items: + json: + x-kubernetes-preserve-unknown-fields: true + properties: + a: {} + int-or-string: + x-kubernetes-int-or-string: true + properties: + a: {} +`, + expectedViolations: []string{ + "spec.validation.openAPIV3Schema.properties[foo].properties[a].type: Required value: must not be empty for specified object fields", + "spec.validation.openAPIV3Schema.properties[foo].type: Required value: must not be empty for specified object fields", + "spec.validation.openAPIV3Schema.properties[int-or-string].properties[a].type: Required value: must not be empty for specified object fields", + "spec.validation.openAPIV3Schema.properties[json].properties[a].type: Required value: must not be empty for specified object fields", + "spec.validation.openAPIV3Schema.properties[abc].additionalProperties.properties[a].items.additionalProperties.type: Required value: must not be empty for specified object fields", + "spec.validation.openAPIV3Schema.properties[abc].additionalProperties.properties[a].items.type: Required value: must not be empty for specified array items", + "spec.validation.openAPIV3Schema.properties[abc].additionalProperties.properties[a].type: Required value: must not be empty for specified object fields", + "spec.validation.openAPIV3Schema.properties[abc].additionalProperties.type: Required value: must not be empty for specified object fields", + "spec.validation.openAPIV3Schema.properties[abc].type: Required value: must not be empty for specified object fields", + "spec.validation.openAPIV3Schema.properties[bar].items.additionalProperties.items.type: Required value: must not be empty for specified array items", + "spec.validation.openAPIV3Schema.properties[bar].items.additionalProperties.properties[a].type: Required value: must not be empty for specified object fields", + "spec.validation.openAPIV3Schema.properties[bar].items.additionalProperties.type: Required value: must not be empty for specified object fields", + "spec.validation.openAPIV3Schema.properties[bar].items.type: Required value: must not be empty for specified array items", + "spec.validation.openAPIV3Schema.properties[bar].type: Required value: must not be empty for specified object fields", + "spec.validation.openAPIV3Schema.type: Required value: must not be empty at the root", + }, + }, + { + desc: "int-or-string variants", + globalSchema: ` +type: object +properties: + a: + x-kubernetes-int-or-string: true + b: + x-kubernetes-int-or-string: true + anyOf: + - type: integer + - type: string + allOf: + - pattern: abc + c: + x-kubernetes-int-or-string: true + allOf: + - anyOf: + - type: integer + - type: string + - pattern: abc + - pattern: abc + d: + x-kubernetes-int-or-string: true + anyOf: + - type: integer + - type: string + pattern: abc + e: + x-kubernetes-int-or-string: true + allOf: + - anyOf: + - type: integer + - type: string + pattern: abc + - pattern: abc + f: + x-kubernetes-int-or-string: true + anyOf: + - type: integer + - type: string + - pattern: abc + g: + x-kubernetes-int-or-string: true + anyOf: + - type: string + - type: integer +`, + expectedViolations: []string{ + "spec.validation.openAPIV3Schema.properties[d].anyOf[0].type: Forbidden: must be empty to be structural", + "spec.validation.openAPIV3Schema.properties[d].anyOf[1].type: Forbidden: must be empty to be structural", + "spec.validation.openAPIV3Schema.properties[e].allOf[0].anyOf[0].type: Forbidden: must be empty to be structural", + "spec.validation.openAPIV3Schema.properties[e].allOf[0].anyOf[1].type: Forbidden: must be empty to be structural", + "spec.validation.openAPIV3Schema.properties[f].anyOf[0].type: Forbidden: must be empty to be structural", + "spec.validation.openAPIV3Schema.properties[f].anyOf[1].type: Forbidden: must be empty to be structural", + "spec.validation.openAPIV3Schema.properties[g].anyOf[0].type: Forbidden: must be empty to be structural", + "spec.validation.openAPIV3Schema.properties[g].anyOf[1].type: Forbidden: must be empty to be structural", + }, + unexpectedViolations: []string{ + "spec.validation.openAPIV3Schema.properties[a]", + "spec.validation.openAPIV3Schema.properties[b]", + "spec.validation.openAPIV3Schema.properties[c]", + }, + }, + { + desc: "forbidden additionalProperties at the root", + globalSchema: ` +type: object +additionalProperties: false +`, + expectedViolations: []string{ + "spec.validation.openAPIV3Schema.additionalProperties: Forbidden: must not be used at the root", + }, + }, + { + desc: "structural incomplete", + globalSchema: ` +type: object +properties: + b: + type: object + properties: + b: + type: array + c: + type: array + items: + type: object + d: + type: array +not: + properties: + a: {} + b: + not: + properties: + a: {} + b: + items: {} + c: + items: + not: + items: + properties: + a: {} + d: + items: {} +allOf: +- properties: + e: {} +anyOf: +- properties: + f: {} +oneOf: +- properties: + g: {} +`, + expectedViolations: []string{ + "spec.validation.openAPIV3Schema.properties[d].items: Required value: because it is defined in spec.validation.openAPIV3Schema.not.properties[d].items", + "spec.validation.openAPIV3Schema.properties[a]: Required value: because it is defined in spec.validation.openAPIV3Schema.not.properties[a]", + "spec.validation.openAPIV3Schema.properties[b].properties[a]: Required value: because it is defined in spec.validation.openAPIV3Schema.not.properties[b].not.properties[a]", + "spec.validation.openAPIV3Schema.properties[b].properties[b].items: Required value: because it is defined in spec.validation.openAPIV3Schema.not.properties[b].not.properties[b].items", + "spec.validation.openAPIV3Schema.properties[c].items.items: Required value: because it is defined in spec.validation.openAPIV3Schema.not.properties[c].items.not.items", + "spec.validation.openAPIV3Schema.properties[e]: Required value: because it is defined in spec.validation.openAPIV3Schema.allOf[0].properties[e]", + "spec.validation.openAPIV3Schema.properties[f]: Required value: because it is defined in spec.validation.openAPIV3Schema.anyOf[0].properties[f]", + "spec.validation.openAPIV3Schema.properties[g]: Required value: because it is defined in spec.validation.openAPIV3Schema.oneOf[0].properties[g]", + }, + }, + { + desc: "structural complete", + globalSchema: ` +type: object +properties: + a: + type: string + b: + type: object + properties: + a: + type: string + b: + type: array + items: + type: string + c: + type: array + items: + type: array + items: + type: object + properties: + a: + type: string + d: + type: array + items: + type: string + e: + type: string + f: + type: string + g: + type: string +not: + properties: + a: {} + b: + not: + properties: + a: {} + b: + items: {} + c: + items: + not: + items: + properties: + a: {} + d: + items: {} +allOf: +- properties: + e: {} +anyOf: +- properties: + f: {} +oneOf: +- properties: + g: {} +`, + expectedViolations: nil, + }, + { + desc: "invalid v1beta1 schema", + v1beta1Schema: ` +type: object +properties: + a: {} +not: + properties: + b: {} +`, + v1Schema: ` +type: object +properties: + a: + type: string +`, + expectedViolations: []string{ + "spec.version[v1beta1].schema.openAPIV3Schema.properties[a].type: Required value: must not be empty for specified object fields", + "spec.version[v1beta1].schema.openAPIV3Schema.properties[b]: Required value: because it is defined in spec.version[v1beta1].schema.openAPIV3Schema.not.properties[b]", + }, + }, + { + desc: "invalid v1beta1 and v1 schemas", + v1beta1Schema: ` +type: object +properties: + a: {} +not: + properties: + b: {} +`, + v1Schema: ` +type: object +properties: + c: {} +not: + properties: + d: {} +`, + expectedViolations: []string{ + "spec.version[v1beta1].schema.openAPIV3Schema.properties[a].type: Required value: must not be empty for specified object fields", + "spec.version[v1beta1].schema.openAPIV3Schema.properties[b]: Required value: because it is defined in spec.version[v1beta1].schema.openAPIV3Schema.not.properties[b]", + "spec.version[v1].schema.openAPIV3Schema.properties[c].type: Required value: must not be empty for specified object fields", + "spec.version[v1].schema.openAPIV3Schema.properties[d]: Required value: because it is defined in spec.version[v1].schema.openAPIV3Schema.not.properties[d]", + }, + }, + } + + for i := range tests { + tst := tests[i] + t.Run(tst.desc, func(t *testing.T) { + // plug in schemas + manifest := strings.NewReplacer( + "GLOBAL_SCHEMA", toValidationJSON(tst.globalSchema), + "V1BETA1_SCHEMA", toValidationJSON(tst.v1beta1Schema), + "V1_SCHEMA", toValidationJSON(tst.v1Schema), + ).Replace(tmpl) + + // decode CRD manifest + obj, _, err := clientschema.Codecs.UniversalDeserializer().Decode([]byte(manifest), nil, nil) + if err != nil { + t.Fatalf("failed decoding of: %v\n\n%s", err, manifest) + } + crd := obj.(*apiextensionsv1beta1.CustomResourceDefinition) + crd.Spec.Group = fmt.Sprintf("tests-%d.apiextension.k8s.io", i) + crd.Name = fmt.Sprintf("foos.%s", crd.Spec.Group) + + // create CRDs + crd, err = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Create(crd) + if tst.expectedCreateError && err == nil { + t.Fatalf("expected error, got none") + } else if !tst.expectedCreateError && err != nil { + t.Fatalf("unexpected create error: %v", err) + } + if err != nil { + return + } + + if len(tst.expectedViolations) == 0 { + // wait for condition to not appear + var cond *apiextensionsv1beta1.CustomResourceDefinitionCondition + err := wait.PollImmediate(100*time.Millisecond, 5*time.Second, func() (bool, error) { + obj, err := apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get(crd.Name, metav1.GetOptions{}) + if err != nil { + return false, err + } + cond = findCRDCondition(obj, apiextensionsv1beta1.NonStructuralSchema) + if cond == nil { + return false, nil + } + return true, nil + }) + if err != wait.ErrWaitTimeout { + t.Fatalf("expected no NonStructuralSchema condition, but got one: %v", cond) + } + return + } + + // wait for condition to appear with the given violations + var cond *apiextensionsv1beta1.CustomResourceDefinitionCondition + err = wait.PollImmediate(100*time.Millisecond, wait.ForeverTestTimeout, func() (bool, error) { + obj, err := apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Get(crd.Name, metav1.GetOptions{}) + if err != nil { + return false, err + } + cond = findCRDCondition(obj, apiextensionsv1beta1.NonStructuralSchema) + if cond != nil { + return true, nil + } + return false, nil + }) + if err != nil { + t.Fatalf("unexpected error waiting for violations in NonStructuralSchema condition: %v", err) + } + + // check that the condition looks good + if cond.Reason != "Violations" { + t.Errorf("expected reason Violations, got: %v", cond.Reason) + } + if cond.Status != apiextensionsv1beta1.ConditionTrue { + t.Errorf("expected reason True, got: %v", cond.Status) + } + + // check that we got all violations + t.Logf("Got violations: %q", cond.Message) + for _, v := range tst.expectedViolations { + if strings.Index(cond.Message, v) == -1 { + t.Errorf("expected violation %q, but didn't get it", v) + } + } + for _, v := range tst.unexpectedViolations { + if strings.Index(cond.Message, v) != -1 { + t.Errorf("unexpected violation %q", v) + } + } + }) + } +} + +// findCRDCondition returns the condition you're looking for or nil. +func findCRDCondition(crd *apiextensionsv1beta1.CustomResourceDefinition, conditionType apiextensionsv1beta1.CustomResourceDefinitionConditionType) *apiextensionsv1beta1.CustomResourceDefinitionCondition { + for i := range crd.Status.Conditions { + if crd.Status.Conditions[i].Type == conditionType { + return &crd.Status.Conditions[i] + } + } + + return nil +} + +func toValidationJSON(yml string) string { + if len(yml) == 0 { + return "null" + } + bs, err := yaml.ToJSON([]byte(yml)) + if err != nil { + panic(err) + } + return fmt.Sprintf("{\"openAPIV3Schema\": %s}", string(bs)) +} + func float64Ptr(f float64) *float64 { return &f } diff --git a/vendor/modules.txt b/vendor/modules.txt index 788f32673db..4623350d8f9 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1066,6 +1066,7 @@ k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1 k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation k8s.io/apiextensions-apiserver/pkg/apiserver k8s.io/apiextensions-apiserver/pkg/apiserver/conversion +k8s.io/apiextensions-apiserver/pkg/apiserver/schema k8s.io/apiextensions-apiserver/pkg/apiserver/validation k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme @@ -1087,6 +1088,7 @@ k8s.io/apiextensions-apiserver/pkg/cmd/server/options k8s.io/apiextensions-apiserver/pkg/cmd/server/testing k8s.io/apiextensions-apiserver/pkg/controller/establish k8s.io/apiextensions-apiserver/pkg/controller/finalizer +k8s.io/apiextensions-apiserver/pkg/controller/nonstructuralschema k8s.io/apiextensions-apiserver/pkg/controller/openapi k8s.io/apiextensions-apiserver/pkg/controller/status k8s.io/apiextensions-apiserver/pkg/crdserverscheme