mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-20 18:31:15 +00:00
adaptor between structral and OpenAPI.
This commit is contained in:
parent
e11ea145ea
commit
7eb7c8b4fa
@ -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)
|
||||
}
|
||||
|
@ -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)}
|
||||
}
|
@ -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
|
||||
|
@ -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})
|
||||
}
|
||||
|
81
staging/src/k8s.io/apiserver/pkg/cel/common/adaptor.go
Normal file
81
staging/src/k8s.io/apiserver/pkg/cel/common/adaptor.go
Normal 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
|
||||
}
|
@ -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)
|
@ -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":
|
@ -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
|
147
staging/src/k8s.io/apiserver/pkg/cel/openapi/adaptor.go
Normal file
147
staging/src/k8s.io/apiserver/pkg/cel/openapi/adaptor.go
Normal 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)
|
||||
}
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user