feat: kubesphere 4.0 (#6115)
* feat: kubesphere 4.0 Signed-off-by: ci-bot <ci-bot@kubesphere.io> * feat: kubesphere 4.0 Signed-off-by: ci-bot <ci-bot@kubesphere.io> --------- Signed-off-by: ci-bot <ci-bot@kubesphere.io> Co-authored-by: ks-ci-bot <ks-ci-bot@example.com> Co-authored-by: joyceliu <joyceliu@yunify.com>
This commit is contained in:
committed by
GitHub
parent
b5015ec7b9
commit
447a51f08b
356
vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation.go
generated
vendored
Normal file
356
vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation.go
generated
vendored
Normal file
@@ -0,0 +1,356 @@
|
||||
/*
|
||||
Copyright 2021 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 cel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/cel-go/cel"
|
||||
"github.com/google/cel-go/checker"
|
||||
"github.com/google/cel-go/common/types"
|
||||
|
||||
apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
||||
"k8s.io/apimachinery/pkg/util/version"
|
||||
celconfig "k8s.io/apiserver/pkg/apis/cel"
|
||||
apiservercel "k8s.io/apiserver/pkg/cel"
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
"k8s.io/apiserver/pkg/cel/library"
|
||||
"k8s.io/apiserver/pkg/cel/metrics"
|
||||
)
|
||||
|
||||
const (
|
||||
// ScopedVarName is the variable name assigned to the locally scoped data element of a CEL validation
|
||||
// expression.
|
||||
ScopedVarName = "self"
|
||||
|
||||
// OldScopedVarName is the variable name assigned to the existing value of the locally scoped data element of a
|
||||
// CEL validation expression.
|
||||
OldScopedVarName = "oldSelf"
|
||||
)
|
||||
|
||||
// CompilationResult represents the cel compilation result for one rule
|
||||
type CompilationResult struct {
|
||||
Program cel.Program
|
||||
Error *apiservercel.Error
|
||||
// If true, the compiled expression contains a reference to the identifier "oldSelf".
|
||||
UsesOldSelf bool
|
||||
// Represents the worst-case cost of the compiled expression in terms of CEL's cost units, as used by cel.EstimateCost.
|
||||
MaxCost uint64
|
||||
// MaxCardinality represents the worse case number of times this validation rule could be invoked if contained under an
|
||||
// unbounded map or list in an OpenAPIv3 schema.
|
||||
MaxCardinality uint64
|
||||
// MessageExpression represents the cel Program that should be evaluated to generate an error message if the rule
|
||||
// fails to validate. If no MessageExpression was given, or if this expression failed to compile, this will be nil.
|
||||
MessageExpression cel.Program
|
||||
// MessageExpressionError represents an error encountered during compilation of MessageExpression. If no error was
|
||||
// encountered, this will be nil.
|
||||
MessageExpressionError *apiservercel.Error
|
||||
// MessageExpressionMaxCost represents the worst-case cost of the compiled MessageExpression in terms of CEL's cost units,
|
||||
// as used by cel.EstimateCost.
|
||||
MessageExpressionMaxCost uint64
|
||||
// NormalizedRuleFieldPath represents the relative fieldPath specified by user after normalization.
|
||||
NormalizedRuleFieldPath string
|
||||
}
|
||||
|
||||
// EnvLoader delegates the decision of which CEL environment to use for each expression.
|
||||
// Callers should return the appropriate CEL environment based on the guidelines from
|
||||
// environment.NewExpressions and environment.StoredExpressions.
|
||||
type EnvLoader interface {
|
||||
// RuleEnv returns the appropriate environment from the EnvSet for the given CEL rule.
|
||||
RuleEnv(envSet *environment.EnvSet, expression string) *cel.Env
|
||||
// MessageExpressionEnv returns the appropriate environment from the EnvSet for the given
|
||||
// CEL messageExpressions.
|
||||
MessageExpressionEnv(envSet *environment.EnvSet, expression string) *cel.Env
|
||||
}
|
||||
|
||||
// NewExpressionsEnvLoader creates an EnvLoader that always uses the NewExpressions environment type.
|
||||
func NewExpressionsEnvLoader() EnvLoader {
|
||||
return alwaysNewEnvLoader{loadFn: func(envSet *environment.EnvSet) *cel.Env {
|
||||
return envSet.NewExpressionsEnv()
|
||||
}}
|
||||
}
|
||||
|
||||
// StoredExpressionsEnvLoader creates an EnvLoader that always uses the StoredExpressions environment type.
|
||||
func StoredExpressionsEnvLoader() EnvLoader {
|
||||
return alwaysNewEnvLoader{loadFn: func(envSet *environment.EnvSet) *cel.Env {
|
||||
return envSet.StoredExpressionsEnv()
|
||||
}}
|
||||
}
|
||||
|
||||
type alwaysNewEnvLoader struct {
|
||||
loadFn func(envSet *environment.EnvSet) *cel.Env
|
||||
}
|
||||
|
||||
func (pe alwaysNewEnvLoader) RuleEnv(envSet *environment.EnvSet, _ string) *cel.Env {
|
||||
return pe.loadFn(envSet)
|
||||
}
|
||||
|
||||
func (pe alwaysNewEnvLoader) MessageExpressionEnv(envSet *environment.EnvSet, _ string) *cel.Env {
|
||||
return pe.loadFn(envSet)
|
||||
}
|
||||
|
||||
// Compile compiles all the XValidations rules (without recursing into the schema) and returns a slice containing a
|
||||
// CompilationResult for each ValidationRule, or an error. declType is expected to be a CEL DeclType corresponding
|
||||
// to the structural schema.
|
||||
// Each CompilationResult may contain:
|
||||
// - non-nil Program, nil Error: The program was compiled successfully
|
||||
// - nil Program, non-nil Error: Compilation resulted in an error
|
||||
// - nil Program, nil Error: The provided rule was empty so compilation was not attempted
|
||||
//
|
||||
// perCallLimit was added for testing purpose only. Callers should always use const PerCallLimit from k8s.io/apiserver/pkg/apis/cel/config.go as input.
|
||||
// baseEnv is used as the base CEL environment, see common.BaseEnvironment.
|
||||
func Compile(s *schema.Structural, declType *apiservercel.DeclType, perCallLimit uint64, baseEnvSet *environment.EnvSet, envLoader EnvLoader) ([]CompilationResult, error) {
|
||||
t := time.Now()
|
||||
defer func() {
|
||||
metrics.Metrics.ObserveCompilation(time.Since(t))
|
||||
}()
|
||||
|
||||
if len(s.Extensions.XValidations) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
celRules := s.Extensions.XValidations
|
||||
|
||||
oldSelfEnvSet, optionalOldSelfEnvSet, err := prepareEnvSet(baseEnvSet, declType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
estimator := newCostEstimator(declType)
|
||||
// compResults is the return value which saves a list of compilation results in the same order as x-kubernetes-validations rules.
|
||||
compResults := make([]CompilationResult, len(celRules))
|
||||
maxCardinality := maxCardinality(declType.MinSerializedSize)
|
||||
for i, rule := range celRules {
|
||||
ruleEnvSet := oldSelfEnvSet
|
||||
if rule.OptionalOldSelf != nil && *rule.OptionalOldSelf {
|
||||
ruleEnvSet = optionalOldSelfEnvSet
|
||||
}
|
||||
compResults[i] = compileRule(s, rule, ruleEnvSet, envLoader, estimator, maxCardinality, perCallLimit)
|
||||
}
|
||||
|
||||
return compResults, nil
|
||||
}
|
||||
|
||||
func prepareEnvSet(baseEnvSet *environment.EnvSet, declType *apiservercel.DeclType) (oldSelfEnvSet *environment.EnvSet, optionalOldSelfEnvSet *environment.EnvSet, err error) {
|
||||
scopedType := declType.MaybeAssignTypeName(generateUniqueSelfTypeName())
|
||||
|
||||
oldSelfEnvSet, err = baseEnvSet.Extend(
|
||||
environment.VersionedOptions{
|
||||
// Feature epoch was actually 1.23, but we artificially set it to 1.0 because these
|
||||
// options should always be present.
|
||||
IntroducedVersion: version.MajorMinor(1, 0),
|
||||
EnvOptions: []cel.EnvOption{
|
||||
cel.Variable(ScopedVarName, scopedType.CelType()),
|
||||
},
|
||||
DeclTypes: []*apiservercel.DeclType{
|
||||
scopedType,
|
||||
},
|
||||
},
|
||||
environment.VersionedOptions{
|
||||
IntroducedVersion: version.MajorMinor(1, 24),
|
||||
EnvOptions: []cel.EnvOption{
|
||||
cel.Variable(OldScopedVarName, scopedType.CelType()),
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
optionalOldSelfEnvSet, err = baseEnvSet.Extend(
|
||||
environment.VersionedOptions{
|
||||
// Feature epoch was actually 1.23, but we artificially set it to 1.0 because these
|
||||
// options should always be present.
|
||||
IntroducedVersion: version.MajorMinor(1, 0),
|
||||
EnvOptions: []cel.EnvOption{
|
||||
cel.Variable(ScopedVarName, scopedType.CelType()),
|
||||
},
|
||||
DeclTypes: []*apiservercel.DeclType{
|
||||
scopedType,
|
||||
},
|
||||
},
|
||||
environment.VersionedOptions{
|
||||
IntroducedVersion: version.MajorMinor(1, 24),
|
||||
EnvOptions: []cel.EnvOption{
|
||||
cel.Variable(OldScopedVarName, types.NewOptionalType(scopedType.CelType())),
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return oldSelfEnvSet, optionalOldSelfEnvSet, nil
|
||||
}
|
||||
|
||||
func compileRule(s *schema.Structural, rule apiextensions.ValidationRule, envSet *environment.EnvSet, envLoader EnvLoader, estimator *library.CostEstimator, maxCardinality uint64, perCallLimit uint64) (compilationResult CompilationResult) {
|
||||
if len(strings.TrimSpace(rule.Rule)) == 0 {
|
||||
// include a compilation result, but leave both program and error nil per documented return semantics of this
|
||||
// function
|
||||
return
|
||||
}
|
||||
ruleEnv := envLoader.RuleEnv(envSet, rule.Rule)
|
||||
ast, issues := ruleEnv.Compile(rule.Rule)
|
||||
if issues != nil {
|
||||
compilationResult.Error = &apiservercel.Error{Type: apiservercel.ErrorTypeInvalid, Detail: "compilation failed: " + issues.String()}
|
||||
return
|
||||
}
|
||||
if ast.OutputType() != cel.BoolType {
|
||||
compilationResult.Error = &apiservercel.Error{Type: apiservercel.ErrorTypeInvalid, Detail: "cel expression must evaluate to a bool"}
|
||||
return
|
||||
}
|
||||
|
||||
checkedExpr, err := cel.AstToCheckedExpr(ast)
|
||||
if err != nil {
|
||||
// should be impossible since env.Compile returned no issues
|
||||
compilationResult.Error = &apiservercel.Error{Type: apiservercel.ErrorTypeInternal, Detail: "unexpected compilation error: " + err.Error()}
|
||||
return
|
||||
}
|
||||
for _, ref := range checkedExpr.ReferenceMap {
|
||||
if ref.Name == OldScopedVarName {
|
||||
compilationResult.UsesOldSelf = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Ideally we could configure the per expression limit at validation time and set it to the remaining overall budget, but we would either need a way to pass in a limit at evaluation time or move program creation to validation time
|
||||
prog, err := ruleEnv.Program(ast,
|
||||
cel.CostLimit(perCallLimit),
|
||||
cel.CostTracking(estimator),
|
||||
cel.InterruptCheckFrequency(celconfig.CheckFrequency),
|
||||
)
|
||||
if err != nil {
|
||||
compilationResult.Error = &apiservercel.Error{Type: apiservercel.ErrorTypeInvalid, Detail: "program instantiation failed: " + err.Error()}
|
||||
return
|
||||
}
|
||||
costEst, err := ruleEnv.EstimateCost(ast, estimator)
|
||||
if err != nil {
|
||||
compilationResult.Error = &apiservercel.Error{Type: apiservercel.ErrorTypeInternal, Detail: "cost estimation failed: " + err.Error()}
|
||||
return
|
||||
}
|
||||
compilationResult.MaxCost = costEst.Max
|
||||
compilationResult.MaxCardinality = maxCardinality
|
||||
compilationResult.Program = prog
|
||||
if rule.MessageExpression != "" {
|
||||
messageEnv := envLoader.MessageExpressionEnv(envSet, rule.MessageExpression)
|
||||
ast, issues := messageEnv.Compile(rule.MessageExpression)
|
||||
if issues != nil {
|
||||
compilationResult.MessageExpressionError = &apiservercel.Error{Type: apiservercel.ErrorTypeInvalid, Detail: "messageExpression compilation failed: " + issues.String()}
|
||||
return
|
||||
}
|
||||
if ast.OutputType() != cel.StringType {
|
||||
compilationResult.MessageExpressionError = &apiservercel.Error{Type: apiservercel.ErrorTypeInvalid, Detail: "messageExpression must evaluate to a string"}
|
||||
return
|
||||
}
|
||||
|
||||
_, err := cel.AstToCheckedExpr(ast)
|
||||
if err != nil {
|
||||
compilationResult.MessageExpressionError = &apiservercel.Error{Type: apiservercel.ErrorTypeInternal, Detail: "unexpected messageExpression compilation error: " + err.Error()}
|
||||
return
|
||||
}
|
||||
|
||||
msgProg, err := messageEnv.Program(ast,
|
||||
cel.CostLimit(perCallLimit),
|
||||
cel.CostTracking(estimator),
|
||||
cel.InterruptCheckFrequency(celconfig.CheckFrequency),
|
||||
)
|
||||
if err != nil {
|
||||
compilationResult.MessageExpressionError = &apiservercel.Error{Type: apiservercel.ErrorTypeInvalid, Detail: "messageExpression instantiation failed: " + err.Error()}
|
||||
return
|
||||
}
|
||||
costEst, err := messageEnv.EstimateCost(ast, estimator)
|
||||
if err != nil {
|
||||
compilationResult.MessageExpressionError = &apiservercel.Error{Type: apiservercel.ErrorTypeInternal, Detail: "cost estimation failed for messageExpression: " + err.Error()}
|
||||
return
|
||||
}
|
||||
compilationResult.MessageExpression = msgProg
|
||||
compilationResult.MessageExpressionMaxCost = costEst.Max
|
||||
}
|
||||
if rule.FieldPath != "" {
|
||||
validFieldPath, err := ValidFieldPath(rule.FieldPath, s)
|
||||
if err == nil {
|
||||
compilationResult.NormalizedRuleFieldPath = validFieldPath.String()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// generateUniqueSelfTypeName creates a placeholder type name to use in a CEL programs for cases
|
||||
// where we do not wish to expose a stable type name to CEL validator rule authors. For this to effectively prevent
|
||||
// developers from depending on the generated name (i.e. using it in CEL programs), it must be changed each time a
|
||||
// CRD is created or updated.
|
||||
func generateUniqueSelfTypeName() string {
|
||||
return fmt.Sprintf("selfType%d", time.Now().Nanosecond())
|
||||
}
|
||||
|
||||
func newCostEstimator(root *apiservercel.DeclType) *library.CostEstimator {
|
||||
return &library.CostEstimator{SizeEstimator: &sizeEstimator{root: root}}
|
||||
}
|
||||
|
||||
type sizeEstimator struct {
|
||||
root *apiservercel.DeclType
|
||||
}
|
||||
|
||||
func (c *sizeEstimator) EstimateSize(element checker.AstNode) *checker.SizeEstimate {
|
||||
if len(element.Path()) == 0 {
|
||||
// Path() can return an empty list, early exit if it does since we can't
|
||||
// provide size estimates when that happens
|
||||
return nil
|
||||
}
|
||||
currentNode := c.root
|
||||
// cut off "self" from path, since we always start there
|
||||
for _, name := range element.Path()[1:] {
|
||||
switch name {
|
||||
case "@items", "@values":
|
||||
if currentNode.ElemType == nil {
|
||||
return nil
|
||||
}
|
||||
currentNode = currentNode.ElemType
|
||||
case "@keys":
|
||||
if currentNode.KeyType == nil {
|
||||
return nil
|
||||
}
|
||||
currentNode = currentNode.KeyType
|
||||
default:
|
||||
field, ok := currentNode.Fields[name]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if field.Type == nil {
|
||||
return nil
|
||||
}
|
||||
currentNode = field.Type
|
||||
}
|
||||
}
|
||||
return &checker.SizeEstimate{Min: 0, Max: uint64(currentNode.MaxElements)}
|
||||
}
|
||||
|
||||
func (c *sizeEstimator) EstimateCallCost(function, overloadID string, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate {
|
||||
return nil
|
||||
}
|
||||
|
||||
// maxCardinality returns the maximum number of times data conforming to the minimum size given could possibly exist in
|
||||
// an object serialized to JSON. For cases where a schema is contained under map or array schemas of unbounded
|
||||
// size, this can be used as an estimate as the worst case number of times data matching the schema could be repeated.
|
||||
// Note that this only assumes a single comma between data elements, so if the schema is contained under only maps,
|
||||
// this estimates a higher cardinality that would be possible. DeclType.MinSerializedSize is meant to be passed to
|
||||
// this function.
|
||||
func maxCardinality(minSize int64) uint64 {
|
||||
sz := minSize + 1 // assume at least one comma between elements
|
||||
return uint64(celconfig.MaxRequestSizeBytes / sz)
|
||||
}
|
||||
30
vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/maplist.go
generated
vendored
Normal file
30
vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/maplist.go
generated
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
Copyright 2022 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 cel
|
||||
|
||||
import (
|
||||
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
||||
"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 common.MapList) {
|
||||
return common.MakeMapList(&model.Structural{Structural: sts}, items)
|
||||
}
|
||||
321
vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model/adaptor.go
generated
vendored
Normal file
321
vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model/adaptor.go
generated
vendored
Normal file
@@ -0,0 +1,321 @@
|
||||
/*
|
||||
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) Pattern() string {
|
||||
if s.Structural.ValueValidation == nil {
|
||||
return ""
|
||||
}
|
||||
return s.Structural.ValueValidation.Pattern
|
||||
}
|
||||
|
||||
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) Minimum() *float64 {
|
||||
if s.Structural.ValueValidation == nil {
|
||||
return nil
|
||||
}
|
||||
return s.Structural.ValueValidation.Minimum
|
||||
}
|
||||
|
||||
func (s *Structural) IsExclusiveMinimum() bool {
|
||||
if s.Structural.ValueValidation == nil {
|
||||
return false
|
||||
}
|
||||
return s.Structural.ValueValidation.ExclusiveMinimum
|
||||
}
|
||||
|
||||
func (s *Structural) Maximum() *float64 {
|
||||
if s.Structural.ValueValidation == nil {
|
||||
return nil
|
||||
}
|
||||
return s.Structural.ValueValidation.Maximum
|
||||
}
|
||||
|
||||
func (s *Structural) IsExclusiveMaximum() bool {
|
||||
if s.Structural.ValueValidation == nil {
|
||||
return false
|
||||
}
|
||||
return s.Structural.ValueValidation.ExclusiveMaximum
|
||||
}
|
||||
|
||||
func (s *Structural) MultipleOf() *float64 {
|
||||
if s.Structural.ValueValidation == nil {
|
||||
return nil
|
||||
}
|
||||
return s.Structural.ValueValidation.MultipleOf
|
||||
}
|
||||
|
||||
func (s *Structural) MinItems() *int64 {
|
||||
if s.Structural.ValueValidation == nil {
|
||||
return nil
|
||||
}
|
||||
return s.Structural.ValueValidation.MinItems
|
||||
}
|
||||
|
||||
func (s *Structural) MaxItems() *int64 {
|
||||
if s.Structural.ValueValidation == nil {
|
||||
return nil
|
||||
}
|
||||
return s.Structural.ValueValidation.MaxItems
|
||||
}
|
||||
|
||||
func (s *Structural) MinLength() *int64 {
|
||||
if s.Structural.ValueValidation == nil {
|
||||
return nil
|
||||
}
|
||||
return s.Structural.ValueValidation.MinLength
|
||||
}
|
||||
|
||||
func (s *Structural) MaxLength() *int64 {
|
||||
if s.Structural.ValueValidation == nil {
|
||||
return nil
|
||||
}
|
||||
return s.Structural.ValueValidation.MaxLength
|
||||
}
|
||||
|
||||
func (s *Structural) MinProperties() *int64 {
|
||||
if s.Structural.ValueValidation == nil {
|
||||
return nil
|
||||
}
|
||||
return s.Structural.ValueValidation.MinProperties
|
||||
}
|
||||
|
||||
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) UniqueItems() bool {
|
||||
// This field is forbidden in structural schema.
|
||||
// but you can just you x-kubernetes-list-type:set to get around it :)
|
||||
return false
|
||||
}
|
||||
|
||||
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) XMapType() string {
|
||||
if s.Structural.XMapType == nil {
|
||||
return ""
|
||||
}
|
||||
return *s.Structural.XMapType
|
||||
}
|
||||
|
||||
func (s *Structural) XListMapKeys() []string {
|
||||
return s.Structural.XListMapKeys
|
||||
}
|
||||
|
||||
func (s *Structural) AllOf() []common.Schema {
|
||||
var res []common.Schema
|
||||
for _, subSchema := range s.Structural.ValueValidation.AllOf {
|
||||
subSchema := subSchema
|
||||
res = append(res, nestedValueValidationToStructural(&subSchema))
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (s *Structural) AnyOf() []common.Schema {
|
||||
var res []common.Schema
|
||||
for _, subSchema := range s.Structural.ValueValidation.AnyOf {
|
||||
subSchema := subSchema
|
||||
res = append(res, nestedValueValidationToStructural(&subSchema))
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (s *Structural) OneOf() []common.Schema {
|
||||
var res []common.Schema
|
||||
for _, subSchema := range s.Structural.ValueValidation.OneOf {
|
||||
subSchema := subSchema
|
||||
res = append(res, nestedValueValidationToStructural(&subSchema))
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (s *Structural) Not() common.Schema {
|
||||
if s.Structural.ValueValidation.Not == nil {
|
||||
return nil
|
||||
}
|
||||
return nestedValueValidationToStructural(s.Structural.ValueValidation.Not)
|
||||
}
|
||||
|
||||
// nestedValueValidationToStructural converts a nested value validation to
|
||||
// an equivalent structural schema instance.
|
||||
//
|
||||
// This lets us avoid needing a separate adaptor for the nested value
|
||||
// validations, and doesn't cost too much since since we are usually exploring the
|
||||
// entire schema anyway.
|
||||
func nestedValueValidationToStructural(nvv *schema.NestedValueValidation) *Structural {
|
||||
var newItems *schema.Structural
|
||||
if nvv.Items != nil {
|
||||
newItems = nestedValueValidationToStructural(nvv.Items).Structural
|
||||
}
|
||||
|
||||
var newProperties map[string]schema.Structural
|
||||
for k, v := range nvv.Properties {
|
||||
if newProperties == nil {
|
||||
newProperties = make(map[string]schema.Structural)
|
||||
}
|
||||
|
||||
v := v
|
||||
newProperties[k] = *nestedValueValidationToStructural(&v).Structural
|
||||
}
|
||||
|
||||
return &Structural{
|
||||
Structural: &schema.Structural{
|
||||
Items: newItems,
|
||||
Properties: newProperties,
|
||||
ValueValidation: &nvv.ValueValidation,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type StructuralValidationRule struct {
|
||||
rule, message, messageExpression, fieldPath string
|
||||
}
|
||||
|
||||
func (s *StructuralValidationRule) Rule() string {
|
||||
return s.rule
|
||||
}
|
||||
func (s *StructuralValidationRule) Message() string {
|
||||
return s.message
|
||||
}
|
||||
func (s *StructuralValidationRule) FieldPath() string {
|
||||
return s.fieldPath
|
||||
}
|
||||
func (s *StructuralValidationRule) MessageExpression() string {
|
||||
return s.messageExpression
|
||||
}
|
||||
|
||||
func (s *Structural) XValidations() []common.ValidationRule {
|
||||
if len(s.Structural.XValidations) == 0 {
|
||||
return nil
|
||||
}
|
||||
result := make([]common.ValidationRule, len(s.Structural.XValidations))
|
||||
for i, v := range s.Structural.XValidations {
|
||||
result[i] = &StructuralValidationRule{rule: v.Rule, message: v.Message, messageExpression: v.MessageExpression, fieldPath: v.FieldPath}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *Structural) WithTypeAndObjectMeta() common.Schema {
|
||||
return &Structural{Structural: WithTypeAndObjectMeta(s.Structural)}
|
||||
}
|
||||
73
vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model/schemas.go
generated
vendored
Normal file
73
vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model/schemas.go
generated
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
Copyright 2022 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 (
|
||||
apiservercel "k8s.io/apiserver/pkg/cel"
|
||||
"k8s.io/apiserver/pkg/cel/common"
|
||||
|
||||
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
||||
)
|
||||
|
||||
// SchemaDeclType converts the structural schema to a CEL declaration, or returns nil if the
|
||||
// structural schema should not be exposed in CEL expressions.
|
||||
// Set isResourceRoot to true for the root of a custom resource or embedded resource.
|
||||
//
|
||||
// Schemas with XPreserveUnknownFields not exposed unless they are objects. Array and "maps" schemas
|
||||
// are not exposed if their items or additionalProperties schemas are not exposed. Object Properties are not exposed
|
||||
// if their schema is not exposed.
|
||||
//
|
||||
// The CEL declaration for objects with XPreserveUnknownFields does not expose unknown fields.
|
||||
func SchemaDeclType(s *schema.Structural, isResourceRoot bool) *apiservercel.DeclType {
|
||||
return common.SchemaDeclType(&Structural{Structural: s}, isResourceRoot)
|
||||
}
|
||||
|
||||
// WithTypeAndObjectMeta ensures the kind, apiVersion and
|
||||
// metadata.name and metadata.generateName properties are specified, making a shallow copy of the provided schema if needed.
|
||||
func WithTypeAndObjectMeta(s *schema.Structural) *schema.Structural {
|
||||
if s.Properties != nil &&
|
||||
s.Properties["kind"].Type == "string" &&
|
||||
s.Properties["apiVersion"].Type == "string" &&
|
||||
s.Properties["metadata"].Type == "object" &&
|
||||
s.Properties["metadata"].Properties != nil &&
|
||||
s.Properties["metadata"].Properties["name"].Type == "string" &&
|
||||
s.Properties["metadata"].Properties["generateName"].Type == "string" {
|
||||
return s
|
||||
}
|
||||
result := &schema.Structural{
|
||||
Generic: s.Generic,
|
||||
Extensions: s.Extensions,
|
||||
ValueValidation: s.ValueValidation,
|
||||
}
|
||||
props := make(map[string]schema.Structural, len(s.Properties))
|
||||
for k, prop := range s.Properties {
|
||||
props[k] = prop
|
||||
}
|
||||
stringType := schema.Structural{Generic: schema.Generic{Type: "string"}}
|
||||
props["kind"] = stringType
|
||||
props["apiVersion"] = stringType
|
||||
props["metadata"] = schema.Structural{
|
||||
Generic: schema.Generic{Type: "object"},
|
||||
Properties: map[string]schema.Structural{
|
||||
"name": stringType,
|
||||
"generateName": stringType,
|
||||
},
|
||||
}
|
||||
result.Properties = props
|
||||
|
||||
return result
|
||||
}
|
||||
789
vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/validation.go
generated
vendored
Normal file
789
vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/validation.go
generated
vendored
Normal file
@@ -0,0 +1,789 @@
|
||||
/*
|
||||
Copyright 2021 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 cel
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
celgo "github.com/google/cel-go/cel"
|
||||
"github.com/google/cel-go/common/types"
|
||||
"github.com/google/cel-go/common/types/ref"
|
||||
"github.com/google/cel-go/interpreter"
|
||||
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/utils/ptr"
|
||||
|
||||
apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model"
|
||||
"k8s.io/apiextensions-apiserver/pkg/features"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/apiserver/pkg/cel"
|
||||
"k8s.io/apiserver/pkg/cel/common"
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
"k8s.io/apiserver/pkg/cel/metrics"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/apiserver/pkg/warning"
|
||||
|
||||
celconfig "k8s.io/apiserver/pkg/apis/cel"
|
||||
)
|
||||
|
||||
// Validator parallels the structure of schema.Structural and includes the compiled CEL programs
|
||||
// for the x-kubernetes-validations of each schema node.
|
||||
type Validator struct {
|
||||
Items *Validator
|
||||
Properties map[string]Validator
|
||||
|
||||
AdditionalProperties *Validator
|
||||
|
||||
compiledRules []CompilationResult
|
||||
|
||||
// Program compilation is pre-checked at CRD creation/update time, so we don't expect compilation to fail
|
||||
// they are recompiled and added to this type, and it does, it is an internal bug.
|
||||
// But if somehow we get any compilation errors, we track them and then surface them as validation errors.
|
||||
compilationErr error
|
||||
|
||||
// isResourceRoot is true if this validator node is for the root of a resource. Either the root of the
|
||||
// custom resource being validated, or the root of an XEmbeddedResource object.
|
||||
isResourceRoot bool
|
||||
|
||||
// celActivationFactory produces a Activations, which resolve identifiers
|
||||
// (e.g. self and oldSelf) to CEL values. One activation must be produced
|
||||
// for each of the cases when oldSelf is optional and non-optional.
|
||||
celActivationFactory func(sts *schema.Structural, obj, oldObj interface{}) (activation interpreter.Activation, optionalOldSelfActivation interpreter.Activation)
|
||||
}
|
||||
|
||||
// NewValidator returns compiles all the CEL programs defined in x-kubernetes-validations extensions
|
||||
// of the Structural schema and returns a custom resource validator that contains nested
|
||||
// validators for all items, properties and additionalProperties that transitively contain validator rules.
|
||||
// Returns nil if there are no validator rules in the Structural schema. May return a validator containing only errors.
|
||||
// Adding perCallLimit as input arg for testing purpose only. Callers should always use const PerCallLimit from k8s.io/apiserver/pkg/apis/cel/config.go as input
|
||||
func NewValidator(s *schema.Structural, isResourceRoot bool, perCallLimit uint64) *Validator {
|
||||
if !hasXValidations(s) {
|
||||
return nil
|
||||
}
|
||||
return validator(s, isResourceRoot, model.SchemaDeclType(s, isResourceRoot), perCallLimit)
|
||||
}
|
||||
|
||||
// validator creates a Validator for all x-kubernetes-validations at the level of the provided schema and lower and
|
||||
// returns the Validator if any x-kubernetes-validations exist in the schema, or nil if no x-kubernetes-validations
|
||||
// exist. declType is expected to be a CEL DeclType corresponding to the structural schema.
|
||||
// perCallLimit was added for testing purpose only. Callers should always use const PerCallLimit from k8s.io/apiserver/pkg/apis/cel/config.go as input.
|
||||
func validator(s *schema.Structural, isResourceRoot bool, declType *cel.DeclType, perCallLimit uint64) *Validator {
|
||||
compiledRules, err := Compile(s, declType, perCallLimit, environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()), StoredExpressionsEnvLoader())
|
||||
var itemsValidator, additionalPropertiesValidator *Validator
|
||||
var propertiesValidators map[string]Validator
|
||||
if s.Items != nil {
|
||||
itemsValidator = validator(s.Items, s.Items.XEmbeddedResource, declType.ElemType, perCallLimit)
|
||||
}
|
||||
if len(s.Properties) > 0 {
|
||||
propertiesValidators = make(map[string]Validator, len(s.Properties))
|
||||
for k, p := range s.Properties {
|
||||
prop := p
|
||||
var fieldType *cel.DeclType
|
||||
if escapedPropName, ok := cel.Escape(k); ok {
|
||||
if f, ok := declType.Fields[escapedPropName]; ok {
|
||||
fieldType = f.Type
|
||||
} else {
|
||||
// fields with unknown types are omitted from CEL validation entirely
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
// field may be absent from declType if the property name is unescapable, in which case we should convert
|
||||
// the field value type to a DeclType.
|
||||
fieldType = model.SchemaDeclType(&prop, prop.XEmbeddedResource)
|
||||
if fieldType == nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if p := validator(&prop, prop.XEmbeddedResource, fieldType, perCallLimit); p != nil {
|
||||
propertiesValidators[k] = *p
|
||||
}
|
||||
}
|
||||
}
|
||||
if s.AdditionalProperties != nil && s.AdditionalProperties.Structural != nil {
|
||||
additionalPropertiesValidator = validator(s.AdditionalProperties.Structural, s.AdditionalProperties.Structural.XEmbeddedResource, declType.ElemType, perCallLimit)
|
||||
}
|
||||
if len(compiledRules) > 0 || err != nil || itemsValidator != nil || additionalPropertiesValidator != nil || len(propertiesValidators) > 0 {
|
||||
activationFactory := validationActivationWithoutOldSelf
|
||||
for _, rule := range compiledRules {
|
||||
if rule.UsesOldSelf {
|
||||
activationFactory = validationActivationWithOldSelf
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return &Validator{
|
||||
compiledRules: compiledRules,
|
||||
compilationErr: err,
|
||||
isResourceRoot: isResourceRoot,
|
||||
Items: itemsValidator,
|
||||
AdditionalProperties: additionalPropertiesValidator,
|
||||
Properties: propertiesValidators,
|
||||
celActivationFactory: activationFactory,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type options struct {
|
||||
ratchetingOptions
|
||||
}
|
||||
|
||||
type Option func(*options)
|
||||
|
||||
func WithRatcheting(correlation *common.CorrelatedObject) Option {
|
||||
return func(o *options) {
|
||||
o.currentCorrelation = correlation
|
||||
}
|
||||
}
|
||||
|
||||
// Validate validates all x-kubernetes-validations rules in Validator against obj and returns any errors.
|
||||
// If the validation rules exceed the costBudget, subsequent evaluations will be skipped, the list of errs returned will not be empty, and a negative remainingBudget will be returned.
|
||||
// Most callers can ignore the returned remainingBudget value unless another validate call is going to be made
|
||||
// context is passed for supporting context cancellation during cel validation
|
||||
func (s *Validator) Validate(ctx context.Context, fldPath *field.Path, sts *schema.Structural, obj, oldObj interface{}, costBudget int64, opts ...Option) (errs field.ErrorList, remainingBudget int64) {
|
||||
opt := options{}
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
return s.validate(ctx, fldPath, sts, obj, oldObj, opt.ratchetingOptions, costBudget)
|
||||
}
|
||||
|
||||
// ratchetingOptions stores the current correlation object and the nearest
|
||||
// parent which was correlatable. The parent is stored so that we can check at
|
||||
// the point an error is thrown whether it should be ratcheted using simple
|
||||
// logic
|
||||
// Key and Index should be used as normally to traverse to the next node.
|
||||
type ratchetingOptions struct {
|
||||
// Current correlation object. If nil, then this node is from an uncorrelatable
|
||||
// part of the schema
|
||||
currentCorrelation *common.CorrelatedObject
|
||||
|
||||
// If currentCorrelation is nil, this is the nearest parent to this node
|
||||
// which was correlatable. If the parent is deepequal to its old value,
|
||||
// then errors thrown by this node are ratcheted
|
||||
nearestParentCorrelation *common.CorrelatedObject
|
||||
}
|
||||
|
||||
// shouldRatchetError returns true if the errors raised by the current node
|
||||
// should be ratcheted.
|
||||
//
|
||||
// Errors for the current node should be ratcheted if one of the following is true:
|
||||
// 1. The current node is correlatable, and it is equal to its old value
|
||||
// 2. The current node has a correlatable ancestor, and the ancestor is equal
|
||||
// to its old value.
|
||||
func (r ratchetingOptions) shouldRatchetError() bool {
|
||||
if r.currentCorrelation != nil {
|
||||
return r.currentCorrelation.CachedDeepEqual()
|
||||
}
|
||||
|
||||
return r.nearestParentCorrelation.CachedDeepEqual()
|
||||
}
|
||||
|
||||
// Finds the next node following the field in the tree and returns options using
|
||||
// that node. If none could be found, then retains a reference to the last
|
||||
// correlatable ancestor for ratcheting purposes
|
||||
func (r ratchetingOptions) key(field string) ratchetingOptions {
|
||||
if r.currentCorrelation == nil {
|
||||
return r
|
||||
}
|
||||
|
||||
return ratchetingOptions{currentCorrelation: r.currentCorrelation.Key(field), nearestParentCorrelation: r.currentCorrelation}
|
||||
}
|
||||
|
||||
// Finds the next node following the index in the tree and returns options using
|
||||
// that node. If none could be found, then retains a reference to the last
|
||||
// correlatable ancestor for ratcheting purposes
|
||||
func (r ratchetingOptions) index(idx int) ratchetingOptions {
|
||||
if r.currentCorrelation == nil {
|
||||
return r
|
||||
}
|
||||
|
||||
return ratchetingOptions{currentCorrelation: r.currentCorrelation.Index(idx), nearestParentCorrelation: r.currentCorrelation}
|
||||
}
|
||||
|
||||
func (s *Validator) validate(ctx context.Context, fldPath *field.Path, sts *schema.Structural, obj, oldObj interface{}, correlation ratchetingOptions, costBudget int64) (errs field.ErrorList, remainingBudget int64) {
|
||||
t := time.Now()
|
||||
defer func() {
|
||||
metrics.Metrics.ObserveEvaluation(time.Since(t))
|
||||
}()
|
||||
remainingBudget = costBudget
|
||||
if s == nil || obj == nil {
|
||||
return nil, remainingBudget
|
||||
}
|
||||
|
||||
errs, remainingBudget = s.validateExpressions(ctx, fldPath, sts, obj, oldObj, correlation, remainingBudget)
|
||||
|
||||
if remainingBudget < 0 {
|
||||
return errs, remainingBudget
|
||||
}
|
||||
|
||||
switch obj := obj.(type) {
|
||||
case []interface{}:
|
||||
oldArray, _ := oldObj.([]interface{})
|
||||
var arrayErrs field.ErrorList
|
||||
arrayErrs, remainingBudget = s.validateArray(ctx, fldPath, sts, obj, oldArray, correlation, remainingBudget)
|
||||
errs = append(errs, arrayErrs...)
|
||||
return errs, remainingBudget
|
||||
case map[string]interface{}:
|
||||
oldMap, _ := oldObj.(map[string]interface{})
|
||||
var mapErrs field.ErrorList
|
||||
mapErrs, remainingBudget = s.validateMap(ctx, fldPath, sts, obj, oldMap, correlation, remainingBudget)
|
||||
errs = append(errs, mapErrs...)
|
||||
return errs, remainingBudget
|
||||
}
|
||||
|
||||
return errs, remainingBudget
|
||||
}
|
||||
|
||||
func (s *Validator) validateExpressions(ctx context.Context, fldPath *field.Path, sts *schema.Structural, obj, oldObj interface{}, correlation ratchetingOptions, costBudget int64) (errs field.ErrorList, remainingBudget int64) {
|
||||
// guard against oldObj being a non-nil interface with a nil value
|
||||
if oldObj != nil {
|
||||
v := reflect.ValueOf(oldObj)
|
||||
switch v.Kind() {
|
||||
case reflect.Map, reflect.Pointer, reflect.Interface, reflect.Slice:
|
||||
if v.IsNil() {
|
||||
oldObj = nil // +k8s:verify-mutation:reason=clone
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
remainingBudget = costBudget
|
||||
if obj == nil {
|
||||
// We only validate non-null values. Rules that need to check for the state of a nullable value or the presence of an optional
|
||||
// field must do so from the surrounding schema. E.g. if an array has nullable string items, a rule on the array
|
||||
// schema can check if items are null, but a rule on the nullable string schema only validates the non-null strings.
|
||||
return nil, remainingBudget
|
||||
}
|
||||
if s.compilationErr != nil {
|
||||
errs = append(errs, field.Invalid(fldPath, sts.Type, fmt.Sprintf("rule compiler initialization error: %v", s.compilationErr)))
|
||||
return errs, remainingBudget
|
||||
}
|
||||
if len(s.compiledRules) == 0 {
|
||||
return nil, remainingBudget // nothing to do
|
||||
}
|
||||
if remainingBudget <= 0 {
|
||||
errs = append(errs, field.Invalid(fldPath, sts.Type, fmt.Sprintf("validation failed due to running out of cost budget, no further validation rules will be run")))
|
||||
return errs, -1
|
||||
}
|
||||
if s.isResourceRoot {
|
||||
sts = model.WithTypeAndObjectMeta(sts)
|
||||
}
|
||||
activation, optionalOldSelfActivation := s.celActivationFactory(sts, obj, oldObj)
|
||||
for i, compiled := range s.compiledRules {
|
||||
rule := sts.XValidations[i]
|
||||
if compiled.Error != nil {
|
||||
errs = append(errs, field.Invalid(fldPath, sts.Type, fmt.Sprintf("rule compile error: %v", compiled.Error)))
|
||||
continue
|
||||
}
|
||||
if compiled.Program == nil {
|
||||
// rule is empty
|
||||
continue
|
||||
}
|
||||
|
||||
// If ratcheting is enabled, allow rule with oldSelf to evaluate
|
||||
// when `optionalOldSelf` is set to true
|
||||
optionalOldSelfRule := ptr.Deref(rule.OptionalOldSelf, false)
|
||||
if compiled.UsesOldSelf && oldObj == nil {
|
||||
// transition rules are evaluated only if there is a comparable existing value
|
||||
// But if the rule uses optional oldSelf and gate is enabled we allow
|
||||
// the rule to be evaluated
|
||||
if !utilfeature.DefaultFeatureGate.Enabled(features.CRDValidationRatcheting) {
|
||||
continue
|
||||
}
|
||||
|
||||
if !optionalOldSelfRule {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
ruleActivation := activation
|
||||
if optionalOldSelfRule {
|
||||
ruleActivation = optionalOldSelfActivation
|
||||
}
|
||||
|
||||
evalResult, evalDetails, err := compiled.Program.ContextEval(ctx, ruleActivation)
|
||||
if evalDetails == nil {
|
||||
errs = append(errs, field.InternalError(fldPath, fmt.Errorf("runtime cost could not be calculated for validation rule: %v, no further validation rules will be run", ruleErrorString(rule))))
|
||||
return errs, -1
|
||||
} else {
|
||||
rtCost := evalDetails.ActualCost()
|
||||
if rtCost == nil {
|
||||
errs = append(errs, field.Invalid(fldPath, sts.Type, fmt.Sprintf("runtime cost could not be calculated for validation rule: %v, no further validation rules will be run", ruleErrorString(rule))))
|
||||
return errs, -1
|
||||
} else {
|
||||
if *rtCost > math.MaxInt64 || int64(*rtCost) > remainingBudget {
|
||||
errs = append(errs, field.Invalid(fldPath, sts.Type, fmt.Sprintf("validation failed due to running out of cost budget, no further validation rules will be run")))
|
||||
return errs, -1
|
||||
}
|
||||
remainingBudget -= int64(*rtCost)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
// see types.Err for list of well defined error types
|
||||
if strings.HasPrefix(err.Error(), "no such overload") {
|
||||
// Most overload errors are caught by the compiler, which provides details on where exactly in the rule
|
||||
// error was found. Here, an overload error has occurred at runtime no details are provided, so we
|
||||
// append a more descriptive error message. This error can only occur when static type checking has
|
||||
// been bypassed. int-or-string is typed as dynamic and so bypasses compiler type checking.
|
||||
errs = append(errs, field.Invalid(fldPath, sts.Type, fmt.Sprintf("'%v': call arguments did not match a supported operator, function or macro signature for rule: %v", err, ruleErrorString(rule))))
|
||||
} else if strings.HasPrefix(err.Error(), "operation cancelled: actual cost limit exceeded") {
|
||||
errs = append(errs, field.Invalid(fldPath, sts.Type, fmt.Sprintf("'%v': no further validation rules will be run due to call cost exceeds limit for rule: %v", err, ruleErrorString(rule))))
|
||||
return errs, -1
|
||||
} else {
|
||||
// no such key: {key}, index out of bounds: {index}, integer overflow, division by zero, ...
|
||||
errs = append(errs, field.Invalid(fldPath, sts.Type, fmt.Sprintf("%v evaluating rule: %v", err, ruleErrorString(rule))))
|
||||
}
|
||||
continue
|
||||
}
|
||||
if evalResult != types.True {
|
||||
if len(compiled.NormalizedRuleFieldPath) > 0 {
|
||||
fldPath = fldPath.Child(compiled.NormalizedRuleFieldPath)
|
||||
}
|
||||
|
||||
addErr := func(e *field.Error) {
|
||||
if !compiled.UsesOldSelf && correlation.shouldRatchetError() {
|
||||
warning.AddWarning(ctx, "", e.Error())
|
||||
} else {
|
||||
errs = append(errs, e)
|
||||
}
|
||||
}
|
||||
|
||||
if compiled.MessageExpression != nil {
|
||||
messageExpression, newRemainingBudget, msgErr := evalMessageExpression(ctx, compiled.MessageExpression, rule.MessageExpression, activation, remainingBudget)
|
||||
if msgErr != nil {
|
||||
if msgErr.Type == cel.ErrorTypeInternal {
|
||||
addErr(field.InternalError(fldPath, msgErr))
|
||||
return errs, -1
|
||||
} else if msgErr.Type == cel.ErrorTypeInvalid {
|
||||
addErr(field.Invalid(fldPath, sts.Type, msgErr.Error()))
|
||||
return errs, -1
|
||||
} else {
|
||||
klog.V(2).ErrorS(msgErr, "messageExpression evaluation failed")
|
||||
addErr(fieldErrorForReason(fldPath, sts.Type, ruleMessageOrDefault(rule), rule.Reason))
|
||||
remainingBudget = newRemainingBudget
|
||||
}
|
||||
} else {
|
||||
addErr(fieldErrorForReason(fldPath, sts.Type, messageExpression, rule.Reason))
|
||||
remainingBudget = newRemainingBudget
|
||||
}
|
||||
} else {
|
||||
addErr(fieldErrorForReason(fldPath, sts.Type, ruleMessageOrDefault(rule), rule.Reason))
|
||||
}
|
||||
}
|
||||
}
|
||||
return errs, remainingBudget
|
||||
}
|
||||
|
||||
var unescapeMatcher = regexp.MustCompile(`\\.`)
|
||||
|
||||
func unescapeSingleQuote(s string) (string, error) {
|
||||
var err error
|
||||
unescaped := unescapeMatcher.ReplaceAllStringFunc(s, func(matchStr string) string {
|
||||
directive := matchStr[1]
|
||||
switch directive {
|
||||
case 'a':
|
||||
return "\a"
|
||||
case 'b':
|
||||
return "\b"
|
||||
case 'f':
|
||||
return "\f"
|
||||
case 'n':
|
||||
return "\n"
|
||||
case 'r':
|
||||
return "\r"
|
||||
case 't':
|
||||
return "\t"
|
||||
case 'v':
|
||||
return "\v"
|
||||
case '\'':
|
||||
return "'"
|
||||
case '\\':
|
||||
return "\\"
|
||||
default:
|
||||
err = fmt.Errorf("invalid escape char %s", matchStr)
|
||||
return ""
|
||||
}
|
||||
})
|
||||
return unescaped, err
|
||||
}
|
||||
|
||||
// ValidFieldPath validates that jsonPath is a valid JSON Path containing only field and map accessors
|
||||
// that are valid for the given schema, and returns a field.Path representation of the validated jsonPath or an error.
|
||||
func ValidFieldPath(jsonPath string, schema *schema.Structural) (validFieldPath *field.Path, err error) {
|
||||
appendToPath := func(name string, isNamed bool) error {
|
||||
if !isNamed {
|
||||
validFieldPath = validFieldPath.Key(name)
|
||||
schema = schema.AdditionalProperties.Structural
|
||||
} else {
|
||||
validFieldPath = validFieldPath.Child(name)
|
||||
val, ok := schema.Properties[name]
|
||||
if !ok {
|
||||
return fmt.Errorf("does not refer to a valid field")
|
||||
}
|
||||
schema = &val
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
validFieldPath = nil
|
||||
|
||||
scanner := bufio.NewScanner(strings.NewReader(jsonPath))
|
||||
|
||||
// configure the scanner to split the string into tokens.
|
||||
// The three delimiters ('.', '[', ']') will be returned as single char tokens.
|
||||
// All other text between delimiters is returned as string tokens.
|
||||
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||
if len(data) > 0 {
|
||||
for i := 0; i < len(data); i++ {
|
||||
// If in a single quoted string, look for the end of string
|
||||
// ignoring delimiters.
|
||||
if data[0] == '\'' {
|
||||
if i > 0 && data[i] == '\'' && data[i-1] != '\\' {
|
||||
// Return quoted string
|
||||
return i + 1, data[:i+1], nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
switch data[i] {
|
||||
case '.', '[', ']': // delimiters
|
||||
if i == 0 {
|
||||
// Return the delimiter.
|
||||
return 1, data[:1], nil
|
||||
} else {
|
||||
// Return identifier leading up to the delimiter.
|
||||
// The next call to split will return the delimiter.
|
||||
return i, data[:i], nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if atEOF {
|
||||
// Return the string.
|
||||
return len(data), data, nil
|
||||
}
|
||||
}
|
||||
return 0, nil, nil
|
||||
})
|
||||
|
||||
var tok string
|
||||
var isNamed bool
|
||||
for scanner.Scan() {
|
||||
tok = scanner.Text()
|
||||
switch tok {
|
||||
case "[":
|
||||
if !scanner.Scan() {
|
||||
return nil, fmt.Errorf("unexpected end of JSON path")
|
||||
}
|
||||
tok = scanner.Text()
|
||||
if len(tok) < 2 || tok[0] != '\'' || tok[len(tok)-1] != '\'' {
|
||||
return nil, fmt.Errorf("expected single quoted string but got %s", tok)
|
||||
}
|
||||
unescaped, err := unescapeSingleQuote(tok[1 : len(tok)-1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid string literal: %v", err)
|
||||
}
|
||||
|
||||
if schema.Properties != nil {
|
||||
isNamed = true
|
||||
} else if schema.AdditionalProperties != nil {
|
||||
isNamed = false
|
||||
} else {
|
||||
return nil, fmt.Errorf("does not refer to a valid field")
|
||||
}
|
||||
if err := appendToPath(unescaped, isNamed); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !scanner.Scan() {
|
||||
return nil, fmt.Errorf("unexpected end of JSON path")
|
||||
}
|
||||
tok = scanner.Text()
|
||||
if tok != "]" {
|
||||
return nil, fmt.Errorf("expected ] but got %s", tok)
|
||||
}
|
||||
case ".":
|
||||
if !scanner.Scan() {
|
||||
return nil, fmt.Errorf("unexpected end of JSON path")
|
||||
}
|
||||
tok = scanner.Text()
|
||||
if schema.Properties != nil {
|
||||
isNamed = true
|
||||
} else if schema.AdditionalProperties != nil {
|
||||
isNamed = false
|
||||
} else {
|
||||
return nil, fmt.Errorf("does not refer to a valid field")
|
||||
}
|
||||
if err := appendToPath(tok, isNamed); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("expected [ or . but got: %s", tok)
|
||||
}
|
||||
}
|
||||
return validFieldPath, nil
|
||||
}
|
||||
|
||||
func fieldErrorForReason(fldPath *field.Path, value interface{}, detail string, reason *apiextensions.FieldValueErrorReason) *field.Error {
|
||||
if reason == nil {
|
||||
return field.Invalid(fldPath, value, detail)
|
||||
}
|
||||
switch *reason {
|
||||
case apiextensions.FieldValueForbidden:
|
||||
return field.Forbidden(fldPath, detail)
|
||||
case apiextensions.FieldValueRequired:
|
||||
return field.Required(fldPath, detail)
|
||||
case apiextensions.FieldValueDuplicate:
|
||||
return field.Duplicate(fldPath, value)
|
||||
default:
|
||||
return field.Invalid(fldPath, value, detail)
|
||||
}
|
||||
}
|
||||
|
||||
// evalMessageExpression evaluates the given message expression and returns the evaluated string form and the remaining budget, or an error if one
|
||||
// occurred during evaluation.
|
||||
func evalMessageExpression(ctx context.Context, expr celgo.Program, exprSrc string, activation interpreter.Activation, remainingBudget int64) (string, int64, *cel.Error) {
|
||||
evalResult, evalDetails, err := expr.ContextEval(ctx, activation)
|
||||
if evalDetails == nil {
|
||||
return "", -1, &cel.Error{
|
||||
Type: cel.ErrorTypeInternal,
|
||||
Detail: fmt.Sprintf("runtime cost could not be calculated for messageExpression: %q", exprSrc),
|
||||
}
|
||||
}
|
||||
rtCost := evalDetails.ActualCost()
|
||||
if rtCost == nil {
|
||||
return "", -1, &cel.Error{
|
||||
Type: cel.ErrorTypeInternal,
|
||||
Detail: fmt.Sprintf("runtime cost could not be calculated for messageExpression: %q", exprSrc),
|
||||
}
|
||||
} else if *rtCost > math.MaxInt64 || int64(*rtCost) > remainingBudget {
|
||||
return "", -1, &cel.Error{
|
||||
Type: cel.ErrorTypeInvalid,
|
||||
Detail: "messageExpression evaluation failed due to running out of cost budget, no further validation rules will be run",
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
if strings.HasPrefix(err.Error(), "operation cancelled: actual cost limit exceeded") {
|
||||
return "", -1, &cel.Error{
|
||||
Type: cel.ErrorTypeInvalid,
|
||||
Detail: fmt.Sprintf("no further validation rules will be run due to call cost exceeds limit for messageExpression: %q", exprSrc),
|
||||
}
|
||||
}
|
||||
return "", remainingBudget - int64(*rtCost), &cel.Error{
|
||||
Detail: fmt.Sprintf("messageExpression evaluation failed due to: %v", err.Error()),
|
||||
}
|
||||
}
|
||||
messageStr, ok := evalResult.Value().(string)
|
||||
if !ok {
|
||||
return "", remainingBudget - int64(*rtCost), &cel.Error{
|
||||
Detail: "messageExpression failed to convert to string",
|
||||
}
|
||||
}
|
||||
trimmedMsgStr := strings.TrimSpace(messageStr)
|
||||
if len(trimmedMsgStr) > celconfig.MaxEvaluatedMessageExpressionSizeBytes {
|
||||
return "", remainingBudget - int64(*rtCost), &cel.Error{
|
||||
Detail: fmt.Sprintf("messageExpression beyond allowable length of %d", celconfig.MaxEvaluatedMessageExpressionSizeBytes),
|
||||
}
|
||||
} else if hasNewlines(trimmedMsgStr) {
|
||||
return "", remainingBudget - int64(*rtCost), &cel.Error{
|
||||
Detail: "messageExpression should not contain line breaks",
|
||||
}
|
||||
} else if len(trimmedMsgStr) == 0 {
|
||||
return "", remainingBudget - int64(*rtCost), &cel.Error{
|
||||
Detail: "messageExpression should evaluate to a non-empty string",
|
||||
}
|
||||
}
|
||||
return trimmedMsgStr, remainingBudget - int64(*rtCost), nil
|
||||
}
|
||||
|
||||
var newlineMatcher = regexp.MustCompile(`[\n]+`)
|
||||
|
||||
func hasNewlines(s string) bool {
|
||||
return newlineMatcher.MatchString(s)
|
||||
}
|
||||
|
||||
func ruleMessageOrDefault(rule apiextensions.ValidationRule) string {
|
||||
if len(rule.Message) == 0 {
|
||||
return fmt.Sprintf("failed rule: %s", ruleErrorString(rule))
|
||||
} else {
|
||||
return strings.TrimSpace(rule.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func ruleErrorString(rule apiextensions.ValidationRule) string {
|
||||
if len(rule.Message) > 0 {
|
||||
return strings.TrimSpace(rule.Message)
|
||||
}
|
||||
return strings.TrimSpace(rule.Rule)
|
||||
}
|
||||
|
||||
type validationActivation struct {
|
||||
self, oldSelf ref.Val
|
||||
hasOldSelf bool
|
||||
}
|
||||
|
||||
func validationActivationWithOldSelf(sts *schema.Structural, obj, oldObj interface{}) (activation interpreter.Activation, optionalOldSelfActivation interpreter.Activation) {
|
||||
va := &validationActivation{
|
||||
self: UnstructuredToVal(obj, sts),
|
||||
}
|
||||
optionalVA := &validationActivation{
|
||||
self: va.self,
|
||||
hasOldSelf: true, // this means the oldSelf variable is defined for CEL to reference, not that it has a value
|
||||
oldSelf: types.OptionalNone,
|
||||
}
|
||||
|
||||
if oldObj != nil {
|
||||
va.oldSelf = UnstructuredToVal(oldObj, sts) // +k8s:verify-mutation:reason=clone
|
||||
va.hasOldSelf = true
|
||||
|
||||
optionalVA.oldSelf = types.OptionalOf(va.oldSelf) // +k8s:verify-mutation:reason=clone
|
||||
}
|
||||
|
||||
return va, optionalVA
|
||||
}
|
||||
|
||||
func validationActivationWithoutOldSelf(sts *schema.Structural, obj, _ interface{}) (interpreter.Activation, interpreter.Activation) {
|
||||
res := &validationActivation{
|
||||
self: UnstructuredToVal(obj, sts),
|
||||
}
|
||||
return res, res
|
||||
}
|
||||
|
||||
func (a *validationActivation) ResolveName(name string) (interface{}, bool) {
|
||||
switch name {
|
||||
case ScopedVarName:
|
||||
return a.self, true
|
||||
case OldScopedVarName:
|
||||
return a.oldSelf, a.hasOldSelf
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
func (a *validationActivation) Parent() interpreter.Activation {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Validator) validateMap(ctx context.Context, fldPath *field.Path, sts *schema.Structural, obj, oldObj map[string]interface{}, correlation ratchetingOptions, costBudget int64) (errs field.ErrorList, remainingBudget int64) {
|
||||
remainingBudget = costBudget
|
||||
if remainingBudget < 0 {
|
||||
return errs, remainingBudget
|
||||
}
|
||||
if s == nil || obj == nil {
|
||||
return nil, remainingBudget
|
||||
}
|
||||
|
||||
correlatable := MapIsCorrelatable(sts.XMapType)
|
||||
|
||||
if s.AdditionalProperties != nil && sts.AdditionalProperties != nil && sts.AdditionalProperties.Structural != nil {
|
||||
for k, v := range obj {
|
||||
var oldV interface{}
|
||||
if correlatable {
|
||||
oldV = oldObj[k] // +k8s:verify-mutation:reason=clone
|
||||
}
|
||||
|
||||
var err field.ErrorList
|
||||
err, remainingBudget = s.AdditionalProperties.validate(ctx, fldPath.Key(k), sts.AdditionalProperties.Structural, v, oldV, correlation.key(k), remainingBudget)
|
||||
errs = append(errs, err...)
|
||||
if remainingBudget < 0 {
|
||||
return errs, remainingBudget
|
||||
}
|
||||
}
|
||||
}
|
||||
if s.Properties != nil && sts.Properties != nil {
|
||||
for k, v := range obj {
|
||||
stsProp, stsOk := sts.Properties[k]
|
||||
sub, ok := s.Properties[k]
|
||||
if ok && stsOk {
|
||||
var oldV interface{}
|
||||
if correlatable {
|
||||
oldV = oldObj[k] // +k8s:verify-mutation:reason=clone
|
||||
}
|
||||
|
||||
var err field.ErrorList
|
||||
err, remainingBudget = sub.validate(ctx, fldPath.Child(k), &stsProp, v, oldV, correlation.key(k), remainingBudget)
|
||||
errs = append(errs, err...)
|
||||
if remainingBudget < 0 {
|
||||
return errs, remainingBudget
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errs, remainingBudget
|
||||
}
|
||||
|
||||
func (s *Validator) validateArray(ctx context.Context, fldPath *field.Path, sts *schema.Structural, obj, oldObj []interface{}, correlation ratchetingOptions, costBudget int64) (errs field.ErrorList, remainingBudget int64) {
|
||||
remainingBudget = costBudget
|
||||
if remainingBudget < 0 {
|
||||
return errs, remainingBudget
|
||||
}
|
||||
|
||||
if s.Items != nil && sts.Items != nil {
|
||||
// only map-type lists support self-oldSelf correlation for cel rules. if this isn't a
|
||||
// map-type list, then makeMapList returns an implementation that always returns nil
|
||||
correlatableOldItems := makeMapList(sts, oldObj)
|
||||
for i := range obj {
|
||||
var err field.ErrorList
|
||||
err, remainingBudget = s.Items.validate(ctx, fldPath.Index(i), sts.Items, obj[i], correlatableOldItems.Get(obj[i]), correlation.index(i), remainingBudget)
|
||||
errs = append(errs, err...)
|
||||
if remainingBudget < 0 {
|
||||
return errs, remainingBudget
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errs, remainingBudget
|
||||
}
|
||||
|
||||
// MapIsCorrelatable returns true if the mapType can be used to correlate the data elements of a map after an update
|
||||
// with the data elements of the map from before the updated.
|
||||
func MapIsCorrelatable(mapType *string) bool {
|
||||
// if a third map type is introduced, assume it's not correlatable. granular is the default if unspecified.
|
||||
return mapType == nil || *mapType == "granular" || *mapType == "atomic"
|
||||
}
|
||||
|
||||
func hasXValidations(s *schema.Structural) bool {
|
||||
if s == nil {
|
||||
return false
|
||||
}
|
||||
if len(s.XValidations) > 0 {
|
||||
return true
|
||||
}
|
||||
if hasXValidations(s.Items) {
|
||||
return true
|
||||
}
|
||||
if s.AdditionalProperties != nil && hasXValidations(s.AdditionalProperties.Structural) {
|
||||
return true
|
||||
}
|
||||
if s.Properties != nil {
|
||||
for _, prop := range s.Properties {
|
||||
if hasXValidations(&prop) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
32
vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/values.go
generated
vendored
Normal file
32
vendor/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/values.go
generated
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
Copyright 2021 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 cel
|
||||
|
||||
import (
|
||||
"github.com/google/cel-go/common/types/ref"
|
||||
|
||||
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, &model.Structural{Structural: schema})
|
||||
}
|
||||
Reference in New Issue
Block a user