adaptor between structral and OpenAPI.

This commit is contained in:
Jiahui Feng 2023-02-06 18:56:51 -08:00
parent e11ea145ea
commit 7eb7c8b4fa
10 changed files with 464 additions and 110 deletions

View File

@ -18,12 +18,13 @@ package cel
import (
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
celopenapi "k8s.io/apiserver/pkg/cel/openapi"
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model"
"k8s.io/apiserver/pkg/cel/common"
)
// makeMapList returns a queryable interface over the provided x-kubernetes-list-type=map
// keyedItems. If the provided schema is _not_ an array with x-kubernetes-list-type=map, returns an
// empty mapList.
func makeMapList(sts *schema.Structural, items []interface{}) (rv celopenapi.MapList) {
return celopenapi.MakeMapList(sts.ToKubeOpenAPI(), items)
func makeMapList(sts *schema.Structural, items []interface{}) (rv common.MapList) {
return common.MakeMapList(&model.Structural{Structural: sts}, items)
}

View File

@ -0,0 +1,152 @@
/*
Copyright 2023 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 model
import (
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
"k8s.io/apiserver/pkg/cel/common"
)
var _ common.Schema = (*Structural)(nil)
var _ common.SchemaOrBool = (*StructuralOrBool)(nil)
type Structural struct {
Structural *schema.Structural
}
type StructuralOrBool struct {
StructuralOrBool *schema.StructuralOrBool
}
func (sb *StructuralOrBool) Schema() common.Schema {
if sb.StructuralOrBool.Structural == nil {
return nil
}
return &Structural{Structural: sb.StructuralOrBool.Structural}
}
func (sb *StructuralOrBool) Allows() bool {
return sb.StructuralOrBool.Bool
}
func (s *Structural) Type() string {
return s.Structural.Type
}
func (s *Structural) Format() string {
if s.Structural.ValueValidation == nil {
return ""
}
return s.Structural.ValueValidation.Format
}
func (s *Structural) Items() common.Schema {
return &Structural{Structural: s.Structural.Items}
}
func (s *Structural) Properties() map[string]common.Schema {
if s.Structural.Properties == nil {
return nil
}
res := make(map[string]common.Schema, len(s.Structural.Properties))
for n, prop := range s.Structural.Properties {
s := prop
res[n] = &Structural{Structural: &s}
}
return res
}
func (s *Structural) AdditionalProperties() common.SchemaOrBool {
if s.Structural.AdditionalProperties == nil {
return nil
}
return &StructuralOrBool{StructuralOrBool: s.Structural.AdditionalProperties}
}
func (s *Structural) Default() any {
return s.Structural.Default.Object
}
func (s *Structural) MaxItems() *int64 {
if s.Structural.ValueValidation == nil {
return nil
}
return s.Structural.ValueValidation.MaxItems
}
func (s *Structural) MaxLength() *int64 {
if s.Structural.ValueValidation == nil {
return nil
}
return s.Structural.ValueValidation.MaxLength
}
func (s *Structural) MaxProperties() *int64 {
if s.Structural.ValueValidation == nil {
return nil
}
return s.Structural.ValueValidation.MaxProperties
}
func (s *Structural) Required() []string {
if s.Structural.ValueValidation == nil {
return nil
}
return s.Structural.ValueValidation.Required
}
func (s *Structural) Enum() []any {
if s.Structural.ValueValidation == nil {
return nil
}
ret := make([]any, 0, len(s.Structural.ValueValidation.Enum))
for _, e := range s.Structural.ValueValidation.Enum {
ret = append(ret, e.Object)
}
return ret
}
func (s *Structural) Nullable() bool {
return s.Structural.Nullable
}
func (s *Structural) IsXIntOrString() bool {
return s.Structural.XIntOrString
}
func (s *Structural) IsXEmbeddedResource() bool {
return s.Structural.XEmbeddedResource
}
func (s *Structural) IsXPreserveUnknownFields() bool {
return s.Structural.XPreserveUnknownFields
}
func (s *Structural) XListType() string {
if s.Structural.XListType == nil {
return ""
}
return *s.Structural.XListType
}
func (s *Structural) XListMapKeys() []string {
return s.Structural.XListMapKeys
}
func (s *Structural) WithTypeAndObjectMeta() common.Schema {
return &Structural{Structural: WithTypeAndObjectMeta(s.Structural)}
}

View File

@ -18,7 +18,7 @@ package model
import (
apiservercel "k8s.io/apiserver/pkg/cel"
celopenapi "k8s.io/apiserver/pkg/cel/openapi"
"k8s.io/apiserver/pkg/cel/common"
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
)
@ -33,7 +33,7 @@ import (
//
// The CEL declaration for objects with XPreserveUnknownFields does not expose unknown fields.
func SchemaDeclType(s *schema.Structural, isResourceRoot bool) *apiservercel.DeclType {
return celopenapi.SchemaDeclType(s.ToKubeOpenAPI(), isResourceRoot)
return common.SchemaDeclType(&Structural{Structural: s}, isResourceRoot)
}
// WithTypeAndObjectMeta ensures the kind, apiVersion and

View File

@ -19,14 +19,14 @@ package cel
import (
"github.com/google/cel-go/common/types/ref"
celopenapi "k8s.io/apiserver/pkg/cel/openapi"
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model"
celopenapi "k8s.io/apiserver/pkg/cel/common"
)
// UnstructuredToVal converts a Kubernetes unstructured data element to a CEL Val.
// The root schema of custom resource schema is expected contain type meta and object meta schemas.
// If Embedded resources do not contain type meta and object meta schemas, they will be added automatically.
func UnstructuredToVal(unstructured interface{}, schema *structuralschema.Structural) ref.Val {
return celopenapi.UnstructuredToVal(unstructured, schema.ToKubeOpenAPI())
return celopenapi.UnstructuredToVal(unstructured, &model.Structural{Structural: schema})
}

View File

@ -0,0 +1,81 @@
/*
Copyright 2023 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 common
// Schema is the adapted type for an OpenAPI schema that CEL uses.
// This schema does not cover all OpenAPI fields but only these CEL requires
// are exposed as getters.
type Schema interface {
// Type returns the OpenAPI type.
// Multiple types are not supported. It should return
// empty string if no type is specified.
Type() string
// Format returns the OpenAPI format. May be empty
Format() string
// Items returns the OpenAPI items. or nil of this field does not exist or
// contains no schema.
Items() Schema
// Properties returns the OpenAPI properties, or nil if this field does not
// exist.
// The values of the returned map are of the adapted type.
Properties() map[string]Schema
// AdditionalProperties returns the OpenAPI additional properties field,
// or nil if this field does not exist.
AdditionalProperties() SchemaOrBool
// Default returns the OpenAPI default field, or nil if this field does not exist.
Default() any
Validations
KubeExtensions
// WithTypeAndObjectMeta returns a schema that has the type and object meta set.
// the type includes "kind", "apiVersion" field
// the "metadata" field requires "name" and "generateName" to be set
// The original schema must not be mutated. Make a copy if necessary.
WithTypeAndObjectMeta() Schema
}
// Validations contains OpenAPI validation that the CEL library uses.
type Validations interface {
MaxItems() *int64
MaxLength() *int64
MaxProperties() *int64
Required() []string
Enum() []any
Nullable() bool
}
// KubeExtensions contains Kubernetes-specific extensions to the OpenAPI schema.
type KubeExtensions interface {
IsXIntOrString() bool
IsXEmbeddedResource() bool
IsXPreserveUnknownFields() bool
XListType() string
XListMapKeys() []string
}
// SchemaOrBool contains either a schema or a boolean indicating if the object
// can contain any fields.
type SchemaOrBool interface {
Schema() Schema
Allows() bool
}

View File

@ -14,13 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package openapi
package common
import (
"fmt"
"strings"
"k8s.io/kube-openapi/pkg/validation/spec"
)
// MapList provides a "lookup by key" operation for lists (arrays) with x-kubernetes-list-type=map.
@ -60,7 +58,7 @@ func (ks *singleKeyStrategy) CompositeKeyFor(obj map[string]interface{}) (interf
// multiKeyStrategy computes a composite key of all key values.
type multiKeyStrategy struct {
sts *spec.Schema
sts Schema
}
// CompositeKeyFor returns a composite key computed from the values of all
@ -69,7 +67,7 @@ func (ks *multiKeyStrategy) CompositeKeyFor(obj map[string]interface{}) (interfa
const keyDelimiter = "\x00" // 0 byte should never appear in the composite key except as delimiter
var delimited strings.Builder
for _, key := range getXListMapKeys(ks.sts) {
for _, key := range ks.sts.XListMapKeys() {
v, ok := obj[key]
if !ok {
return nil, false
@ -99,7 +97,7 @@ func (emptyMapList) Get(interface{}) interface{} {
}
type mapListImpl struct {
sts *spec.Schema
sts Schema
ks keyStrategy
// keyedItems contains all lazily keyed map items
keyedItems map[interface{}]interface{}
@ -148,8 +146,8 @@ func (a *mapListImpl) Get(obj interface{}) interface{} {
return nil
}
func makeKeyStrategy(sts *spec.Schema) keyStrategy {
listMapKeys := getXListMapKeys(sts)
func makeKeyStrategy(sts Schema) keyStrategy {
listMapKeys := sts.XListMapKeys()
if len(listMapKeys) == 1 {
key := listMapKeys[0]
return &singleKeyStrategy{
@ -165,8 +163,8 @@ func makeKeyStrategy(sts *spec.Schema) keyStrategy {
// MakeMapList returns a queryable interface over the provided x-kubernetes-list-type=map
// keyedItems. If the provided schema is _not_ an array with x-kubernetes-list-type=map, returns an
// empty mapList.
func MakeMapList(sts *spec.Schema, items []interface{}) (rv MapList) {
if !sts.Type.Contains("array") || getXListType(sts) != "map" || len(getXListMapKeys(sts)) == 0 {
func MakeMapList(sts Schema, items []interface{}) (rv MapList) {
if sts.Type() != "array" || sts.XListType() != "map" || len(sts.XListMapKeys()) == 0 || len(items) == 0 {
return emptyMapList{}
}
ks := makeKeyStrategy(sts)

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package openapi
package common
import (
"time"
@ -37,11 +37,11 @@ const maxRequestSizeBytes = apiservercel.DefaultMaxRequestSizeBytes
// if their schema is not exposed.
//
// The CEL declaration for objects with XPreserveUnknownFields does not expose unknown fields.
func SchemaDeclType(s *spec.Schema, isResourceRoot bool) *apiservercel.DeclType {
func SchemaDeclType(s Schema, isResourceRoot bool) *apiservercel.DeclType {
if s == nil {
return nil
}
if isXIntOrString(s) {
if s.IsXIntOrString() {
// schemas using XIntOrString are not required to have a type.
// intOrStringType represents the x-kubernetes-int-or-string union type in CEL expressions.
@ -67,24 +67,19 @@ func SchemaDeclType(s *spec.Schema, isResourceRoot bool) *apiservercel.DeclType
// 'apiVersion', 'kind', 'metadata.name' and 'metadata.generateName' are always accessible to validator rules
// at the root of resources, even if not specified in the schema.
// This includes the root of a custom resource and the root of XEmbeddedResource objects.
s = WithTypeAndObjectMeta(s)
s = s.WithTypeAndObjectMeta()
}
// If the schema is not an "int-or-string", type must present.
if len(s.Type) == 0 {
return nil
}
switch s.Type[0] {
switch s.Type() {
case "array":
if s.Items != nil {
itemsType := SchemaDeclType(s.Items.Schema, isXEmbeddedResource(s.Items.Schema))
if s.Items() != nil {
itemsType := SchemaDeclType(s.Items(), s.Items().IsXEmbeddedResource())
if itemsType == nil {
return nil
}
var maxItems int64
if s.MaxItems != nil {
maxItems = zeroIfNegative(*s.MaxItems)
if s.MaxItems() != nil {
maxItems = zeroIfNegative(*s.MaxItems())
} else {
maxItems = estimateMaxArrayItemsFromMinSize(itemsType.MinSerializedSize)
}
@ -92,12 +87,12 @@ func SchemaDeclType(s *spec.Schema, isResourceRoot bool) *apiservercel.DeclType
}
return nil
case "object":
if s.AdditionalProperties != nil && s.AdditionalProperties.Schema != nil {
propsType := SchemaDeclType(s.AdditionalProperties.Schema, isXEmbeddedResource(s.AdditionalProperties.Schema))
if s.AdditionalProperties() != nil && s.AdditionalProperties().Schema() != nil {
propsType := SchemaDeclType(s.AdditionalProperties().Schema(), s.AdditionalProperties().Schema().IsXEmbeddedResource())
if propsType != nil {
var maxProperties int64
if s.MaxProperties != nil {
maxProperties = zeroIfNegative(*s.MaxProperties)
if s.MaxProperties() != nil {
maxProperties = zeroIfNegative(*s.MaxProperties())
} else {
maxProperties = estimateMaxAdditionalPropertiesFromMinSize(propsType.MinSerializedSize)
}
@ -105,32 +100,32 @@ func SchemaDeclType(s *spec.Schema, isResourceRoot bool) *apiservercel.DeclType
}
return nil
}
fields := make(map[string]*apiservercel.DeclField, len(s.Properties))
fields := make(map[string]*apiservercel.DeclField, len(s.Properties()))
required := map[string]bool{}
if s.Required != nil {
for _, f := range s.Required {
if s.Required() != nil {
for _, f := range s.Required() {
required[f] = true
}
}
// an object will always be serialized at least as {}, so account for that
minSerializedSize := int64(2)
for name, prop := range s.Properties {
for name, prop := range s.Properties() {
var enumValues []interface{}
if prop.Enum != nil {
for _, e := range prop.Enum {
if prop.Enum() != nil {
for _, e := range prop.Enum() {
enumValues = append(enumValues, e)
}
}
if fieldType := SchemaDeclType(&prop, isXEmbeddedResource(&prop)); fieldType != nil {
if fieldType := SchemaDeclType(prop, prop.IsXEmbeddedResource()); fieldType != nil {
if propName, ok := apiservercel.Escape(name); ok {
fields[propName] = apiservercel.NewDeclField(propName, fieldType, required[name], enumValues, prop.Default)
fields[propName] = apiservercel.NewDeclField(propName, fieldType, required[name], enumValues, prop.Default())
}
// the min serialized size for an object is 2 (for {}) plus the min size of all its required
// properties
// only include required properties without a default value; default values are filled in
// server-side
if required[name] && prop.Default == nil {
if required[name] && prop.Default() == nil {
minSerializedSize += int64(len(name)) + fieldType.MinSerializedSize + 4
}
}
@ -139,11 +134,11 @@ func SchemaDeclType(s *spec.Schema, isResourceRoot bool) *apiservercel.DeclType
objType.MinSerializedSize = minSerializedSize
return objType
case "string":
switch s.Format {
switch s.Format() {
case "byte":
byteWithMaxLength := apiservercel.NewSimpleTypeWithMinSize("bytes", cel.BytesType, types.Bytes([]byte{}), apiservercel.MinStringSize)
if s.MaxLength != nil {
byteWithMaxLength.MaxElements = zeroIfNegative(*s.MaxLength)
if s.MaxLength() != nil {
byteWithMaxLength.MaxElements = zeroIfNegative(*s.MaxLength())
} else {
byteWithMaxLength.MaxElements = estimateMaxStringLengthPerRequest(s)
}
@ -163,12 +158,12 @@ func SchemaDeclType(s *spec.Schema, isResourceRoot bool) *apiservercel.DeclType
}
strWithMaxLength := apiservercel.NewSimpleTypeWithMinSize("string", cel.StringType, types.String(""), apiservercel.MinStringSize)
if s.MaxLength != nil {
if s.MaxLength() != nil {
// multiply the user-provided max length by 4 in the case of an otherwise-untyped string
// we do this because the OpenAPIv3 spec indicates that maxLength is specified in runes/code points,
// but we need to reason about length for things like request size, so we use bytes in this code (and an individual
// unicode code point can be up to 4 bytes long)
strWithMaxLength.MaxElements = zeroIfNegative(*s.MaxLength) * 4
strWithMaxLength.MaxElements = zeroIfNegative(*s.MaxLength()) * 4
} else {
strWithMaxLength.MaxElements = estimateMaxStringLengthPerRequest(s)
}
@ -227,11 +222,11 @@ func WithTypeAndObjectMeta(s *spec.Schema) *spec.Schema {
// estimateMaxStringLengthPerRequest estimates the maximum string length (in characters)
// of a string compatible with the format requirements in the provided schema.
// must only be called on schemas of type "string" or x-kubernetes-int-or-string: true
func estimateMaxStringLengthPerRequest(s *spec.Schema) int64 {
if isXIntOrString(s) {
func estimateMaxStringLengthPerRequest(s Schema) int64 {
if s.IsXIntOrString() {
return maxRequestSizeBytes - 2
}
switch s.Format {
switch s.Format() {
case "duration":
return apiservercel.MaxDurationSizeJSON
case "date":

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package openapi
package common
import (
"fmt"
@ -28,21 +28,20 @@ import (
"k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apiserver/pkg/cel"
"k8s.io/kube-openapi/pkg/validation/spec"
"k8s.io/kube-openapi/pkg/validation/strfmt"
)
// UnstructuredToVal converts a Kubernetes unstructured data element to a CEL Val.
// The root schema of custom resource schema is expected contain type meta and object meta schemas.
// If Embedded resources do not contain type meta and object meta schemas, they will be added automatically.
func UnstructuredToVal(unstructured interface{}, schema *spec.Schema) ref.Val {
func UnstructuredToVal(unstructured interface{}, schema Schema) ref.Val {
if unstructured == nil {
if schema.Nullable {
if schema.Nullable() {
return types.NullValue
}
return types.NewErr("invalid data, got null for schema with nullable=false")
}
if isXIntOrString(schema) {
if schema.IsXIntOrString() {
switch v := unstructured.(type) {
case string:
return types.String(v)
@ -55,42 +54,42 @@ func UnstructuredToVal(unstructured interface{}, schema *spec.Schema) ref.Val {
}
return types.NewErr("invalid data, expected XIntOrString value to be either a string or integer")
}
if schema.Type.Contains("object") {
if schema.Type() == "object" {
m, ok := unstructured.(map[string]interface{})
if !ok {
return types.NewErr("invalid data, expected a map for the provided schema with type=object")
}
if isXEmbeddedResource(schema) || schema.Properties != nil {
if isXEmbeddedResource(schema) {
schema = WithTypeAndObjectMeta(schema)
if schema.IsXEmbeddedResource() || schema.Properties() != nil {
if schema.IsXEmbeddedResource() {
schema = schema.WithTypeAndObjectMeta()
}
return &unstructuredMap{
value: m,
schema: schema,
propSchema: func(key string) (*spec.Schema, bool) {
if schema, ok := schema.Properties[key]; ok {
return &schema, true
propSchema: func(key string) (Schema, bool) {
if schema, ok := schema.Properties()[key]; ok {
return schema, true
}
return nil, false
},
}
}
if schema.AdditionalProperties != nil && schema.AdditionalProperties.Schema != nil {
if schema.AdditionalProperties() != nil && schema.AdditionalProperties().Schema() != nil {
return &unstructuredMap{
value: m,
schema: schema,
propSchema: func(key string) (*spec.Schema, bool) {
return schema.AdditionalProperties.Schema, true
propSchema: func(key string) (Schema, bool) {
return schema.AdditionalProperties().Schema(), true
},
}
}
// A object with x-kubernetes-preserve-unknown-fields but no properties or additionalProperties is treated
// as an empty object.
if isXPreserveUnknownFields(schema) {
if schema.IsXPreserveUnknownFields() {
return &unstructuredMap{
value: m,
schema: schema,
propSchema: func(key string) (*spec.Schema, bool) {
propSchema: func(key string) (Schema, bool) {
return nil, false
},
}
@ -98,20 +97,20 @@ func UnstructuredToVal(unstructured interface{}, schema *spec.Schema) ref.Val {
return types.NewErr("invalid object type, expected either Properties or AdditionalProperties with Allows=true and non-empty Schema")
}
if schema.Type.Contains("array") {
if schema.Type() == "array" {
l, ok := unstructured.([]interface{})
if !ok {
return types.NewErr("invalid data, expected an array for the provided schema with type=array")
}
if schema.Items == nil {
if schema.Items() == nil {
return types.NewErr("invalid array type, expected Items with a non-empty Schema")
}
typedList := unstructuredList{elements: l, itemsSchema: schema.Items.Schema}
listType := getXListType(schema)
typedList := unstructuredList{elements: l, itemsSchema: schema.Items()}
listType := schema.XListType()
if listType != "" {
switch listType {
case "map":
mapKeys := getXListMapKeys(schema)
mapKeys := schema.XListMapKeys()
return &unstructuredMapList{unstructuredList: typedList, escapedKeyProps: escapeKeyProps(mapKeys)}
case "set":
return &unstructuredSetList{unstructuredList: typedList}
@ -124,12 +123,12 @@ func UnstructuredToVal(unstructured interface{}, schema *spec.Schema) ref.Val {
return &typedList
}
if schema.Type.Contains("string") {
if schema.Type() == "string" {
str, ok := unstructured.(string)
if !ok {
return types.NewErr("invalid data, expected string, got %T", unstructured)
}
switch schema.Format {
switch schema.Format() {
case "duration":
d, err := strfmt.ParseDuration(str)
if err != nil {
@ -159,7 +158,7 @@ func UnstructuredToVal(unstructured interface{}, schema *spec.Schema) ref.Val {
return types.String(str)
}
if schema.Type.Contains("number") {
if schema.Type() == "number" {
switch v := unstructured.(type) {
// float representations of whole numbers (e.g. 1.0, 0.0) can convert to int representations (e.g. 1, 0) in yaml
// to json translation, and then get parsed as int64s
@ -178,7 +177,7 @@ func UnstructuredToVal(unstructured interface{}, schema *spec.Schema) ref.Val {
return types.NewErr("invalid data, expected float, got %T", unstructured)
}
}
if schema.Type.Contains("integer") {
if schema.Type() == "integer" {
switch v := unstructured.(type) {
case int:
return types.Int(v)
@ -190,7 +189,7 @@ func UnstructuredToVal(unstructured interface{}, schema *spec.Schema) ref.Val {
return types.NewErr("invalid data, expected int, got %T", unstructured)
}
}
if schema.Type.Contains("boolean") {
if schema.Type() == "boolean" {
b, ok := unstructured.(bool)
if !ok {
return types.NewErr("invalid data, expected bool, got %T", unstructured)
@ -198,11 +197,11 @@ func UnstructuredToVal(unstructured interface{}, schema *spec.Schema) ref.Val {
return types.Bool(b)
}
if isXPreserveUnknownFields(schema) {
if schema.IsXPreserveUnknownFields() {
return &unknownPreserved{u: unstructured}
}
return types.NewErr("invalid type, expected object, array, number, integer, boolean or string, or no type with x-kubernetes-int-or-string or x-kubernetes-preserve-unknown-fields is true, got %s", schema.Type)
return types.NewErr("invalid type, expected object, array, number, integer, boolean or string, or no type with x-kubernetes-int-or-string or x-kubernetes-preserve-unknown-fields is true, got %s", schema.Type())
}
// unknownPreserved represents unknown data preserved in custom resources via x-kubernetes-preserve-unknown-fields.
@ -418,7 +417,7 @@ func (t *unstructuredSetList) Add(other ref.Val) ref.Val {
// unstructuredList represents an unstructured data instance of an OpenAPI array with x-kubernetes-list-type=atomic (the default).
type unstructuredList struct {
elements []interface{}
itemsSchema *spec.Schema
itemsSchema Schema
}
var _ = traits.Lister(&unstructuredList{})
@ -548,9 +547,9 @@ func (t *unstructuredList) Size() ref.Val {
// unstructuredMap represented an unstructured data instance of an OpenAPI object.
type unstructuredMap struct {
value map[string]interface{}
schema *spec.Schema
schema Schema
// propSchema finds the schema to use for a particular map key.
propSchema func(key string) (*spec.Schema, bool)
propSchema func(key string) (Schema, bool)
}
var _ = traits.Mapper(&unstructuredMap{})
@ -636,7 +635,7 @@ func (t *unstructuredMap) Get(key ref.Val) ref.Val {
}
func (t *unstructuredMap) Iterator() traits.Iterator {
isObject := t.schema.Properties != nil
isObject := t.schema.Properties() != nil
keys := make([]ref.Val, len(t.value))
i := 0
for k := range t.value {
@ -675,7 +674,7 @@ func (t *unstructuredMap) Size() ref.Val {
}
func (t *unstructuredMap) Find(key ref.Val) (ref.Val, bool) {
isObject := t.schema.Properties != nil
isObject := t.schema.Properties() != nil
keyStr, ok := key.(types.String)
if !ok {
return types.MaybeNoSuchOverloadErr(key), true

View File

@ -0,0 +1,147 @@
/*
Copyright 2023 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 openapi
import (
"github.com/google/cel-go/common/types/ref"
apiservercel "k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/common"
"k8s.io/kube-openapi/pkg/validation/spec"
)
var _ common.Schema = (*Schema)(nil)
var _ common.SchemaOrBool = (*SchemaOrBool)(nil)
type Schema struct {
Schema *spec.Schema
}
type SchemaOrBool struct {
SchemaOrBool *spec.SchemaOrBool
}
func (sb *SchemaOrBool) Schema() common.Schema {
return &Schema{Schema: sb.SchemaOrBool.Schema}
}
func (sb *SchemaOrBool) Allows() bool {
return sb.SchemaOrBool.Allows
}
func (s *Schema) Type() string {
if len(s.Schema.Type) == 0 {
return ""
}
return s.Schema.Type[0]
}
func (s *Schema) Format() string {
return s.Schema.Format
}
func (s *Schema) Items() common.Schema {
if s.Schema.Items == nil || s.Schema.Items.Schema == nil {
return nil
}
return &Schema{Schema: s.Schema.Items.Schema}
}
func (s *Schema) Properties() map[string]common.Schema {
if s.Schema.Properties == nil {
return nil
}
res := make(map[string]common.Schema, len(s.Schema.Properties))
for n, prop := range s.Schema.Properties {
// map value is unaddressable, create a shallow copy
// this is a shallow non-recursive copy
s := prop
res[n] = &Schema{Schema: &s}
}
return res
}
func (s *Schema) AdditionalProperties() common.SchemaOrBool {
if s.Schema.AdditionalProperties == nil {
return nil
}
return &SchemaOrBool{SchemaOrBool: s.Schema.AdditionalProperties}
}
func (s *Schema) Default() any {
return s.Schema.Default
}
func (s *Schema) MaxItems() *int64 {
return s.Schema.MaxItems
}
func (s *Schema) MaxLength() *int64 {
return s.Schema.MaxLength
}
func (s *Schema) MaxProperties() *int64 {
return s.Schema.MaxProperties
}
func (s *Schema) Required() []string {
return s.Schema.Required
}
func (s *Schema) Enum() []any {
return s.Schema.Enum
}
func (s *Schema) Nullable() bool {
return s.Schema.Nullable
}
func (s *Schema) IsXIntOrString() bool {
return isXIntOrString(s.Schema)
}
func (s *Schema) IsXEmbeddedResource() bool {
return isXEmbeddedResource(s.Schema)
}
func (s *Schema) IsXPreserveUnknownFields() bool {
return isXPreserveUnknownFields(s.Schema)
}
func (s *Schema) XListType() string {
return getXListType(s.Schema)
}
func (s *Schema) XListMapKeys() []string {
return getXListMapKeys(s.Schema)
}
func (s *Schema) WithTypeAndObjectMeta() common.Schema {
return &Schema{common.WithTypeAndObjectMeta(s.Schema)}
}
func UnstructuredToVal(unstructured any, schema *spec.Schema) ref.Val {
return common.UnstructuredToVal(unstructured, &Schema{schema})
}
func SchemaDeclType(s *spec.Schema, isResourceRoot bool) *apiservercel.DeclType {
return common.SchemaDeclType(&Schema{Schema: s}, isResourceRoot)
}
func MakeMapList(sts *spec.Schema, items []interface{}) (rv common.MapList) {
return common.MakeMapList(&Schema{Schema: sts}, items)
}

View File

@ -29,6 +29,8 @@ func isExtension(schema *spec.Schema, key string) bool {
}
func isXIntOrString(schema *spec.Schema) bool {
// built-in types have the Format while CRDs use extension
// both are valid, checking both
return schema.Format == intOrStringFormat || isExtension(schema, extIntOrString)
}
@ -46,32 +48,11 @@ func getXListType(schema *spec.Schema) string {
}
func getXListMapKeys(schema *spec.Schema) []string {
items, ok := schema.Extensions[extListMapKeys]
mapKeys, ok := schema.Extensions.GetStringSlice(extListMapKeys)
if !ok {
return nil
}
// items may be any of
// - a slice of string
// - a slice of interface{}, a.k.a any, but item's real type is string
// there is no direct conversion, so do that manually
switch items.(type) {
case []string:
return items.([]string)
case []any:
a := items.([]any)
result := make([]string, 0, len(a))
for _, item := range a {
// item must be a string
s, ok := item.(string)
if !ok {
return nil
}
result = append(result, s)
}
return result
}
// no further attempt of handling unexpected type
return nil
return mapKeys
}
const extIntOrString = "x-kubernetes-int-or-string"