diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/maplist.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/maplist.go index 99d9f505914..32cdf86fef4 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/maplist.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/maplist.go @@ -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) } diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model/adaptor.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model/adaptor.go new file mode 100644 index 00000000000..e3e940afa0b --- /dev/null +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model/adaptor.go @@ -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)} +} diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model/schemas.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model/schemas.go index 1bd8713026f..6b49e67a404 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model/schemas.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model/schemas.go @@ -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 diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/values.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/values.go index 8bd4bb8e5b3..8b879eaf206 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/values.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/values.go @@ -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}) } diff --git a/staging/src/k8s.io/apiserver/pkg/cel/common/adaptor.go b/staging/src/k8s.io/apiserver/pkg/cel/common/adaptor.go new file mode 100644 index 00000000000..c28d6ce510a --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/cel/common/adaptor.go @@ -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 +} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/openapi/maplist.go b/staging/src/k8s.io/apiserver/pkg/cel/common/maplist.go similarity index 91% rename from staging/src/k8s.io/apiserver/pkg/cel/openapi/maplist.go rename to staging/src/k8s.io/apiserver/pkg/cel/common/maplist.go index ff67c9eed92..99fda092e4b 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/openapi/maplist.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/common/maplist.go @@ -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) diff --git a/staging/src/k8s.io/apiserver/pkg/cel/openapi/schemas.go b/staging/src/k8s.io/apiserver/pkg/cel/common/schemas.go similarity index 86% rename from staging/src/k8s.io/apiserver/pkg/cel/openapi/schemas.go rename to staging/src/k8s.io/apiserver/pkg/cel/common/schemas.go index 28a3663af23..3fdd3a6c8ba 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/openapi/schemas.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/common/schemas.go @@ -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": diff --git a/staging/src/k8s.io/apiserver/pkg/cel/openapi/values.go b/staging/src/k8s.io/apiserver/pkg/cel/common/values.go similarity index 93% rename from staging/src/k8s.io/apiserver/pkg/cel/openapi/values.go rename to staging/src/k8s.io/apiserver/pkg/cel/common/values.go index b435f98c64e..e6d7b99757e 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/openapi/values.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/common/values.go @@ -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 diff --git a/staging/src/k8s.io/apiserver/pkg/cel/openapi/adaptor.go b/staging/src/k8s.io/apiserver/pkg/cel/openapi/adaptor.go new file mode 100644 index 00000000000..0e2cc6e2b2e --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/cel/openapi/adaptor.go @@ -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) +} diff --git a/staging/src/k8s.io/apiserver/pkg/cel/openapi/extensions.go b/staging/src/k8s.io/apiserver/pkg/cel/openapi/extensions.go index e7817cfe230..6a2f830320b 100644 --- a/staging/src/k8s.io/apiserver/pkg/cel/openapi/extensions.go +++ b/staging/src/k8s.io/apiserver/pkg/cel/openapi/extensions.go @@ -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"