mirror of
https://github.com/optim-enterprises-bv/kubernetes.git
synced 2025-11-02 11:18:16 +00:00
Merge pull request #116779 from jpbetz/cel-ratcheting
Controlled rollout of CEL libraries and language feautres
This commit is contained in:
@@ -32,10 +32,11 @@ import (
|
||||
plugincel "k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
|
||||
celconfig "k8s.io/apiserver/pkg/apis/cel"
|
||||
"k8s.io/apiserver/pkg/cel"
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
"k8s.io/apiserver/pkg/util/webhook"
|
||||
"k8s.io/client-go/util/jsonpath"
|
||||
|
||||
"k8s.io/kubernetes/pkg/apis/admissionregistration"
|
||||
admissionregistrationv1 "k8s.io/kubernetes/pkg/apis/admissionregistration/v1"
|
||||
admissionregistrationv1beta1 "k8s.io/kubernetes/pkg/apis/admissionregistration/v1beta1"
|
||||
@@ -258,6 +259,60 @@ type validationOptions struct {
|
||||
requireRecognizedAdmissionReviewVersion bool
|
||||
requireUniqueWebhookNames bool
|
||||
allowInvalidLabelValueInSelector bool
|
||||
preexistingExpressions preexistingExpressions
|
||||
}
|
||||
|
||||
type preexistingExpressions struct {
|
||||
matchConditionExpressions sets.Set[string]
|
||||
validationExpressions sets.Set[string]
|
||||
validationMessageExpressions sets.Set[string]
|
||||
auditAnnotationValuesExpressions sets.Set[string]
|
||||
}
|
||||
|
||||
func newPreexistingExpressions() preexistingExpressions {
|
||||
return preexistingExpressions{
|
||||
matchConditionExpressions: sets.New[string](),
|
||||
validationExpressions: sets.New[string](),
|
||||
validationMessageExpressions: sets.New[string](),
|
||||
auditAnnotationValuesExpressions: sets.New[string](),
|
||||
}
|
||||
}
|
||||
|
||||
func findMutatingPreexistingExpressions(mutating *admissionregistration.MutatingWebhookConfiguration) preexistingExpressions {
|
||||
preexisting := newPreexistingExpressions()
|
||||
for _, wh := range mutating.Webhooks {
|
||||
for _, mc := range wh.MatchConditions {
|
||||
preexisting.matchConditionExpressions.Insert(mc.Expression)
|
||||
}
|
||||
}
|
||||
return preexisting
|
||||
}
|
||||
|
||||
func findValidatingPreexistingExpressions(validating *admissionregistration.ValidatingWebhookConfiguration) preexistingExpressions {
|
||||
preexisting := newPreexistingExpressions()
|
||||
for _, wh := range validating.Webhooks {
|
||||
for _, mc := range wh.MatchConditions {
|
||||
preexisting.matchConditionExpressions.Insert(mc.Expression)
|
||||
}
|
||||
}
|
||||
return preexisting
|
||||
}
|
||||
|
||||
func findValidatingPolicyPreexistingExpressions(validatingPolicy *admissionregistration.ValidatingAdmissionPolicy) preexistingExpressions {
|
||||
preexisting := newPreexistingExpressions()
|
||||
for _, mc := range validatingPolicy.Spec.MatchConditions {
|
||||
preexisting.matchConditionExpressions.Insert(mc.Expression)
|
||||
}
|
||||
for _, v := range validatingPolicy.Spec.Validations {
|
||||
preexisting.validationExpressions.Insert(v.Expression)
|
||||
if len(v.MessageExpression) > 0 {
|
||||
preexisting.validationMessageExpressions.Insert(v.MessageExpression)
|
||||
}
|
||||
}
|
||||
for _, a := range validatingPolicy.Spec.AuditAnnotations {
|
||||
preexisting.auditAnnotationValuesExpressions.Insert(a.ValueExpression)
|
||||
}
|
||||
return preexisting
|
||||
}
|
||||
|
||||
func validateMutatingWebhookConfiguration(e *admissionregistration.MutatingWebhookConfiguration, opts validationOptions) field.ErrorList {
|
||||
@@ -630,6 +685,7 @@ func ValidateValidatingWebhookConfigurationUpdate(newC, oldC *admissionregistrat
|
||||
requireRecognizedAdmissionReviewVersion: validatingHasAcceptedAdmissionReviewVersions(oldC.Webhooks),
|
||||
requireUniqueWebhookNames: validatingHasUniqueWebhookNames(oldC.Webhooks),
|
||||
allowInvalidLabelValueInSelector: validatingWebhookHasInvalidLabelValueInSelector(oldC.Webhooks),
|
||||
preexistingExpressions: findValidatingPreexistingExpressions(oldC),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -642,6 +698,7 @@ func ValidateMutatingWebhookConfigurationUpdate(newC, oldC *admissionregistratio
|
||||
requireRecognizedAdmissionReviewVersion: mutatingHasAcceptedAdmissionReviewVersions(oldC.Webhooks),
|
||||
requireUniqueWebhookNames: mutatingHasUniqueWebhookNames(oldC.Webhooks),
|
||||
allowInvalidLabelValueInSelector: mutatingWebhookHasInvalidLabelValueInSelector(oldC.Webhooks),
|
||||
preexistingExpressions: findMutatingPreexistingExpressions(oldC),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -692,7 +749,7 @@ func validateValidatingAdmissionPolicySpec(meta metav1.ObjectMeta, spec *admissi
|
||||
allErrors = append(allErrors, field.Required(fldPath.Child("auditAnnotations"), "validations or auditAnnotations must contain at least one item"))
|
||||
} else {
|
||||
for i, validation := range spec.Validations {
|
||||
allErrors = append(allErrors, validateValidation(&validation, spec.ParamKind, fldPath.Child("validations").Index(i))...)
|
||||
allErrors = append(allErrors, validateValidation(&validation, spec.ParamKind, opts, fldPath.Child("validations").Index(i))...)
|
||||
}
|
||||
if spec.AuditAnnotations != nil {
|
||||
keys := sets.NewString()
|
||||
@@ -700,7 +757,7 @@ func validateValidatingAdmissionPolicySpec(meta metav1.ObjectMeta, spec *admissi
|
||||
allErrors = append(allErrors, field.Invalid(fldPath.Child("auditAnnotations"), spec.AuditAnnotations, fmt.Sprintf("must not have more than %d auditAnnotations", maxAuditAnnotations)))
|
||||
}
|
||||
for i, auditAnnotation := range spec.AuditAnnotations {
|
||||
allErrors = append(allErrors, validateAuditAnnotation(meta, &auditAnnotation, spec.ParamKind, fldPath.Child("auditAnnotations").Index(i))...)
|
||||
allErrors = append(allErrors, validateAuditAnnotation(meta, &auditAnnotation, spec.ParamKind, opts, fldPath.Child("auditAnnotations").Index(i))...)
|
||||
if keys.Has(auditAnnotation.Key) {
|
||||
allErrors = append(allErrors, field.Duplicate(fldPath.Child("auditAnnotations").Index(i).Child("key"), auditAnnotation.Key))
|
||||
}
|
||||
@@ -718,13 +775,13 @@ func validateParamKind(gvk admissionregistration.ParamKind, fldPath *field.Path)
|
||||
} else if gv, err := parseGroupVersion(gvk.APIVersion); err != nil {
|
||||
allErrors = append(allErrors, field.Invalid(fldPath.Child("apiVersion"), gvk.APIVersion, err.Error()))
|
||||
} else {
|
||||
//this matches the APIService group field validation
|
||||
// this matches the APIService group field validation
|
||||
if len(gv.Group) > 0 {
|
||||
if errs := utilvalidation.IsDNS1123Subdomain(gv.Group); len(errs) > 0 {
|
||||
allErrors = append(allErrors, field.Invalid(fldPath.Child("apiVersion"), gv.Group, strings.Join(errs, ",")))
|
||||
}
|
||||
}
|
||||
//this matches the APIService version field validation
|
||||
// this matches the APIService version field validation
|
||||
if len(gv.Version) == 0 {
|
||||
allErrors = append(allErrors, field.Invalid(fldPath.Child("apiVersion"), gvk.APIVersion, "version must be specified"))
|
||||
} else {
|
||||
@@ -868,7 +925,7 @@ func validateMatchCondition(v *admissionregistration.MatchCondition, opts valida
|
||||
if len(trimmedExpression) == 0 {
|
||||
allErrors = append(allErrors, field.Required(fldPath.Child("expression"), ""))
|
||||
} else {
|
||||
allErrors = append(allErrors, validateMatchConditionsExpression(trimmedExpression, opts.allowParamsInMatchConditions, fldPath.Child("expression"))...)
|
||||
allErrors = append(allErrors, validateMatchConditionsExpression(trimmedExpression, opts, fldPath.Child("expression"))...)
|
||||
}
|
||||
if len(v.Name) == 0 {
|
||||
allErrors = append(allErrors, field.Required(fldPath.Child("name"), ""))
|
||||
@@ -878,7 +935,7 @@ func validateMatchCondition(v *admissionregistration.MatchCondition, opts valida
|
||||
return allErrors
|
||||
}
|
||||
|
||||
func validateValidation(v *admissionregistration.Validation, paramKind *admissionregistration.ParamKind, fldPath *field.Path) field.ErrorList {
|
||||
func validateValidation(v *admissionregistration.Validation, paramKind *admissionregistration.ParamKind, opts validationOptions, fldPath *field.Path) field.ErrorList {
|
||||
var allErrors field.ErrorList
|
||||
trimmedExpression := strings.TrimSpace(v.Expression)
|
||||
trimmedMsg := strings.TrimSpace(v.Message)
|
||||
@@ -886,14 +943,14 @@ func validateValidation(v *admissionregistration.Validation, paramKind *admissio
|
||||
if len(trimmedExpression) == 0 {
|
||||
allErrors = append(allErrors, field.Required(fldPath.Child("expression"), "expression is not specified"))
|
||||
} else {
|
||||
allErrors = append(allErrors, validateValidationExpression(v.Expression, paramKind != nil, fldPath.Child("expression"))...)
|
||||
allErrors = append(allErrors, validateValidationExpression(v.Expression, paramKind != nil, opts, fldPath.Child("expression"))...)
|
||||
}
|
||||
if len(v.MessageExpression) > 0 && len(trimmedMessageExpression) == 0 {
|
||||
allErrors = append(allErrors, field.Invalid(fldPath.Child("messageExpression"), v.MessageExpression, "must be non-empty if specified"))
|
||||
} else if len(trimmedMessageExpression) != 0 {
|
||||
// use v.MessageExpression instead of trimmedMessageExpression so that
|
||||
// the compiler output shows the correct column.
|
||||
allErrors = append(allErrors, validateMessageExpression(v.MessageExpression, paramKind != nil, fldPath.Child("messageExpression"))...)
|
||||
allErrors = append(allErrors, validateMessageExpression(v.MessageExpression, opts, fldPath.Child("messageExpression"))...)
|
||||
}
|
||||
if len(v.Message) > 0 && len(trimmedMsg) == 0 {
|
||||
allErrors = append(allErrors, field.Invalid(fldPath.Child("message"), v.Message, "message must be non-empty if specified"))
|
||||
@@ -908,9 +965,10 @@ func validateValidation(v *admissionregistration.Validation, paramKind *admissio
|
||||
return allErrors
|
||||
}
|
||||
|
||||
func validateCELCondition(expression plugincel.ExpressionAccessor, variables plugincel.OptionalVariableDeclarations, fldPath *field.Path) field.ErrorList {
|
||||
func validateCELCondition(expression plugincel.ExpressionAccessor, variables plugincel.OptionalVariableDeclarations, envType environment.Type, fldPath *field.Path) field.ErrorList {
|
||||
var allErrors field.ErrorList
|
||||
result := plugincel.CompileCELExpression(expression, variables, celconfig.PerCallLimit)
|
||||
|
||||
result := compiler.CompileCELExpression(expression, variables, envType)
|
||||
if result.Error != nil {
|
||||
switch result.Error.Type {
|
||||
case cel.ErrorTypeRequired:
|
||||
@@ -926,34 +984,46 @@ func validateCELCondition(expression plugincel.ExpressionAccessor, variables plu
|
||||
return allErrors
|
||||
}
|
||||
|
||||
func validateValidationExpression(expression string, hasParams bool, fldPath *field.Path) field.ErrorList {
|
||||
func validateValidationExpression(expression string, hasParams bool, opts validationOptions, fldPath *field.Path) field.ErrorList {
|
||||
envType := environment.NewExpressions
|
||||
if opts.preexistingExpressions.validationExpressions.Has(expression) {
|
||||
envType = environment.StoredExpressions
|
||||
}
|
||||
return validateCELCondition(&validatingadmissionpolicy.ValidationCondition{
|
||||
Expression: expression,
|
||||
}, plugincel.OptionalVariableDeclarations{
|
||||
HasParams: hasParams,
|
||||
HasAuthorizer: true,
|
||||
}, fldPath)
|
||||
}, envType, fldPath)
|
||||
}
|
||||
|
||||
func validateMatchConditionsExpression(expression string, hasParams bool, fldPath *field.Path) field.ErrorList {
|
||||
func validateMatchConditionsExpression(expression string, opts validationOptions, fldPath *field.Path) field.ErrorList {
|
||||
envType := environment.NewExpressions
|
||||
if opts.preexistingExpressions.matchConditionExpressions.Has(expression) {
|
||||
envType = environment.StoredExpressions
|
||||
}
|
||||
return validateCELCondition(&matchconditions.MatchCondition{
|
||||
Expression: expression,
|
||||
}, plugincel.OptionalVariableDeclarations{
|
||||
HasParams: hasParams,
|
||||
HasParams: opts.allowParamsInMatchConditions,
|
||||
HasAuthorizer: true,
|
||||
}, fldPath)
|
||||
}, envType, fldPath)
|
||||
}
|
||||
|
||||
func validateMessageExpression(expression string, hasParams bool, fldPath *field.Path) field.ErrorList {
|
||||
func validateMessageExpression(expression string, opts validationOptions, fldPath *field.Path) field.ErrorList {
|
||||
envType := environment.NewExpressions
|
||||
if opts.preexistingExpressions.validationMessageExpressions.Has(expression) {
|
||||
envType = environment.StoredExpressions
|
||||
}
|
||||
return validateCELCondition(&validatingadmissionpolicy.MessageExpressionCondition{
|
||||
MessageExpression: expression,
|
||||
}, plugincel.OptionalVariableDeclarations{
|
||||
HasParams: hasParams,
|
||||
HasParams: opts.allowParamsInMatchConditions,
|
||||
HasAuthorizer: false,
|
||||
}, fldPath)
|
||||
}, envType, fldPath)
|
||||
}
|
||||
|
||||
func validateAuditAnnotation(meta metav1.ObjectMeta, v *admissionregistration.AuditAnnotation, paramKind *admissionregistration.ParamKind, fldPath *field.Path) field.ErrorList {
|
||||
func validateAuditAnnotation(meta metav1.ObjectMeta, v *admissionregistration.AuditAnnotation, paramKind *admissionregistration.ParamKind, opts validationOptions, fldPath *field.Path) field.ErrorList {
|
||||
var allErrors field.ErrorList
|
||||
if len(meta.GetName()) != 0 {
|
||||
name := meta.GetName()
|
||||
@@ -968,9 +1038,13 @@ func validateAuditAnnotation(meta metav1.ObjectMeta, v *admissionregistration.Au
|
||||
} else if len(trimmedValueExpression) > maxAuditAnnotationValueExpressionLength {
|
||||
allErrors = append(allErrors, field.Required(fldPath.Child("valueExpression"), fmt.Sprintf("must not exceed %d bytes in length", maxAuditAnnotationValueExpressionLength)))
|
||||
} else {
|
||||
result := plugincel.CompileCELExpression(&validatingadmissionpolicy.AuditAnnotationCondition{
|
||||
envType := environment.NewExpressions
|
||||
if opts.preexistingExpressions.auditAnnotationValuesExpressions.Has(v.ValueExpression) {
|
||||
envType = environment.StoredExpressions
|
||||
}
|
||||
result := compiler.CompileCELExpression(&validatingadmissionpolicy.AuditAnnotationCondition{
|
||||
ValueExpression: trimmedValueExpression,
|
||||
}, plugincel.OptionalVariableDeclarations{HasParams: paramKind != nil, HasAuthorizer: true}, celconfig.PerCallLimit)
|
||||
}, plugincel.OptionalVariableDeclarations{HasParams: paramKind != nil, HasAuthorizer: true}, envType)
|
||||
if result.Error != nil {
|
||||
switch result.Error.Type {
|
||||
case cel.ErrorTypeRequired:
|
||||
@@ -1032,7 +1106,10 @@ func validateParamRef(pr *admissionregistration.ParamRef, fldPath *field.Path) f
|
||||
|
||||
// ValidateValidatingAdmissionPolicyUpdate validates update of validating admission policy
|
||||
func ValidateValidatingAdmissionPolicyUpdate(newC, oldC *admissionregistration.ValidatingAdmissionPolicy) field.ErrorList {
|
||||
return validateValidatingAdmissionPolicy(newC, validationOptions{ignoreMatchConditions: ignoreValidatingAdmissionPolicyMatchConditions(newC, oldC)})
|
||||
return validateValidatingAdmissionPolicy(newC, validationOptions{
|
||||
ignoreMatchConditions: ignoreValidatingAdmissionPolicyMatchConditions(newC, oldC),
|
||||
preexistingExpressions: findValidatingPolicyPreexistingExpressions(oldC),
|
||||
})
|
||||
}
|
||||
|
||||
// ValidateValidatingAdmissionPolicyStatusUpdate validates update of status of validating admission policy
|
||||
@@ -1088,3 +1165,5 @@ func validateFieldRef(fieldRef string, fldPath *field.Path) field.ErrorList {
|
||||
// no further checks, for an easier upgrade/rollback
|
||||
return nil
|
||||
}
|
||||
|
||||
var compiler = plugincel.NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()))
|
||||
|
||||
@@ -21,8 +21,14 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/cel-go/cel"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/apimachinery/pkg/util/version"
|
||||
plugincel "k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
"k8s.io/apiserver/pkg/cel/library"
|
||||
"k8s.io/kubernetes/pkg/apis/admissionregistration"
|
||||
)
|
||||
|
||||
@@ -828,6 +834,7 @@ func TestValidateValidatingWebhookConfiguration(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestValidateValidatingWebhookConfigurationUpdate(t *testing.T) {
|
||||
noSideEffect := admissionregistration.SideEffectClassNone
|
||||
unknownSideEffect := admissionregistration.SideEffectClassUnknown
|
||||
validClientConfig := admissionregistration.WebhookClientConfig{
|
||||
URL: strPtr("https://example.com"),
|
||||
@@ -945,6 +952,48 @@ func TestValidateValidatingWebhookConfigurationUpdate(t *testing.T) {
|
||||
},
|
||||
}, true),
|
||||
expectedError: ``,
|
||||
}, {
|
||||
name: "Webhooks must compile CEL expressions with StoredExpression environment if unchanged",
|
||||
config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{
|
||||
Name: "webhook.k8s.io",
|
||||
ClientConfig: validClientConfig,
|
||||
SideEffects: &noSideEffect,
|
||||
MatchConditions: []admissionregistration.MatchCondition{{
|
||||
Name: "checkStorage",
|
||||
Expression: "test() == true",
|
||||
}},
|
||||
},
|
||||
}, true),
|
||||
oldconfig: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{
|
||||
Name: "webhook.k8s.io",
|
||||
ClientConfig: validClientConfig,
|
||||
SideEffects: &noSideEffect,
|
||||
MatchConditions: []admissionregistration.MatchCondition{{
|
||||
Name: "checkStorage",
|
||||
Expression: "test() == true",
|
||||
}}},
|
||||
}, true),
|
||||
}, {
|
||||
name: "Webhooks must compile CEL expressions with NewExpression environment type if changed",
|
||||
config: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{
|
||||
Name: "webhook.k8s.io",
|
||||
ClientConfig: validClientConfig,
|
||||
SideEffects: &noSideEffect,
|
||||
MatchConditions: []admissionregistration.MatchCondition{{
|
||||
Name: "checkStorage",
|
||||
Expression: "test() == true",
|
||||
}}},
|
||||
}, true),
|
||||
oldconfig: newValidatingWebhookConfiguration([]admissionregistration.ValidatingWebhook{{
|
||||
Name: "webhook.k8s.io",
|
||||
ClientConfig: validClientConfig,
|
||||
SideEffects: &noSideEffect,
|
||||
MatchConditions: []admissionregistration.MatchCondition{{
|
||||
Name: "checkStorage",
|
||||
Expression: "true",
|
||||
}}},
|
||||
}, true),
|
||||
expectedError: `undeclared reference to 'test'`,
|
||||
}}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
@@ -1907,7 +1956,53 @@ func TestValidateMutatingWebhookConfigurationUpdate(t *testing.T) {
|
||||
},
|
||||
}, true),
|
||||
expectedError: ``,
|
||||
}}
|
||||
}, {
|
||||
name: "Webhooks must compile CEL expressions with StoredExpression environment if unchanged",
|
||||
config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{
|
||||
Name: "webhook.k8s.io",
|
||||
ClientConfig: validClientConfig,
|
||||
SideEffects: &noSideEffect,
|
||||
MatchConditions: []admissionregistration.MatchCondition{{
|
||||
Name: "checkStorage",
|
||||
Expression: "test() == true",
|
||||
}},
|
||||
},
|
||||
}, true),
|
||||
oldconfig: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{
|
||||
Name: "webhook.k8s.io",
|
||||
ClientConfig: validClientConfig,
|
||||
SideEffects: &noSideEffect,
|
||||
MatchConditions: []admissionregistration.MatchCondition{{
|
||||
Name: "checkStorage",
|
||||
Expression: "test() == true",
|
||||
}},
|
||||
},
|
||||
}, true),
|
||||
},
|
||||
{
|
||||
name: "Webhooks must compile CEL expressions with NewExpression environment if changed",
|
||||
config: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{
|
||||
Name: "webhook.k8s.io",
|
||||
ClientConfig: validClientConfig,
|
||||
SideEffects: &noSideEffect,
|
||||
MatchConditions: []admissionregistration.MatchCondition{{
|
||||
Name: "checkStorage",
|
||||
Expression: "test() == true",
|
||||
},
|
||||
}},
|
||||
}, true),
|
||||
oldconfig: newMutatingWebhookConfiguration([]admissionregistration.MutatingWebhook{{
|
||||
Name: "webhook.k8s.io",
|
||||
ClientConfig: validClientConfig,
|
||||
SideEffects: &noSideEffect,
|
||||
MatchConditions: []admissionregistration.MatchCondition{{
|
||||
Name: "checkStorage",
|
||||
Expression: "true",
|
||||
},
|
||||
}},
|
||||
}, true),
|
||||
expectedError: `undeclared reference to 'test'`,
|
||||
}}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
errs := ValidateMutatingWebhookConfigurationUpdate(test.config, test.oldconfig)
|
||||
@@ -3068,8 +3163,146 @@ func TestValidateValidatingAdmissionPolicyUpdate(t *testing.T) {
|
||||
},
|
||||
expectedError: "",
|
||||
},
|
||||
// TODO: CustomAuditAnnotations: string valueExpression with {oldObject} is allowed
|
||||
{
|
||||
name: "expressions that are not changed must be compiled using the StoredExpression environment",
|
||||
oldconfig: validatingAdmissionPolicyWithExpressions(
|
||||
[]admissionregistration.MatchCondition{
|
||||
{
|
||||
Name: "checkEnvironmentMode",
|
||||
Expression: `test() == true`,
|
||||
},
|
||||
},
|
||||
[]admissionregistration.Validation{
|
||||
{
|
||||
Expression: `test() == true`,
|
||||
MessageExpression: "string(test())",
|
||||
},
|
||||
},
|
||||
[]admissionregistration.AuditAnnotation{
|
||||
{
|
||||
Key: "checkEnvironmentMode",
|
||||
ValueExpression: "string(test())",
|
||||
},
|
||||
}),
|
||||
config: validatingAdmissionPolicyWithExpressions(
|
||||
[]admissionregistration.MatchCondition{
|
||||
{
|
||||
Name: "checkEnvironmentMode",
|
||||
Expression: `test() == true`,
|
||||
},
|
||||
},
|
||||
[]admissionregistration.Validation{
|
||||
{
|
||||
Expression: `test() == true`,
|
||||
MessageExpression: "string(test())",
|
||||
},
|
||||
},
|
||||
[]admissionregistration.AuditAnnotation{
|
||||
{
|
||||
Key: "checkEnvironmentMode",
|
||||
ValueExpression: "string(test())",
|
||||
},
|
||||
}),
|
||||
expectedError: "",
|
||||
},
|
||||
{
|
||||
name: "matchCondition expressions that are changed must be compiled using the NewExpression environment",
|
||||
oldconfig: validatingAdmissionPolicyWithExpressions(
|
||||
[]admissionregistration.MatchCondition{
|
||||
{
|
||||
Name: "checkEnvironmentMode",
|
||||
Expression: `true`,
|
||||
},
|
||||
},
|
||||
nil, nil),
|
||||
config: validatingAdmissionPolicyWithExpressions(
|
||||
[]admissionregistration.MatchCondition{
|
||||
{
|
||||
Name: "checkEnvironmentMode",
|
||||
Expression: `test() == true`,
|
||||
},
|
||||
},
|
||||
nil, nil),
|
||||
expectedError: `undeclared reference to 'test'`,
|
||||
},
|
||||
{
|
||||
name: "validation expressions that are changed must be compiled using the NewExpression environment",
|
||||
oldconfig: validatingAdmissionPolicyWithExpressions(
|
||||
nil,
|
||||
[]admissionregistration.Validation{
|
||||
{
|
||||
Expression: `true`,
|
||||
},
|
||||
},
|
||||
nil),
|
||||
config: validatingAdmissionPolicyWithExpressions(
|
||||
nil,
|
||||
[]admissionregistration.Validation{
|
||||
{
|
||||
Expression: `test() == true`,
|
||||
},
|
||||
},
|
||||
nil),
|
||||
expectedError: `undeclared reference to 'test'`,
|
||||
},
|
||||
{
|
||||
name: "validation messageExpressions that are changed must be compiled using the NewExpression environment",
|
||||
oldconfig: validatingAdmissionPolicyWithExpressions(
|
||||
nil,
|
||||
[]admissionregistration.Validation{
|
||||
{
|
||||
Expression: `true`,
|
||||
MessageExpression: "'test'",
|
||||
},
|
||||
},
|
||||
nil),
|
||||
config: validatingAdmissionPolicyWithExpressions(
|
||||
nil,
|
||||
[]admissionregistration.Validation{
|
||||
{
|
||||
Expression: `true`,
|
||||
MessageExpression: "string(test())",
|
||||
},
|
||||
},
|
||||
nil),
|
||||
expectedError: `undeclared reference to 'test'`,
|
||||
},
|
||||
{
|
||||
name: "auditAnnotation valueExpressions that are changed must be compiled using the NewExpression environment",
|
||||
oldconfig: validatingAdmissionPolicyWithExpressions(
|
||||
nil, nil,
|
||||
[]admissionregistration.AuditAnnotation{
|
||||
{
|
||||
Key: "checkEnvironmentMode",
|
||||
ValueExpression: "'test'",
|
||||
},
|
||||
}),
|
||||
config: validatingAdmissionPolicyWithExpressions(
|
||||
nil, nil,
|
||||
[]admissionregistration.AuditAnnotation{
|
||||
{
|
||||
Key: "checkEnvironmentMode",
|
||||
ValueExpression: "string(test())",
|
||||
},
|
||||
}),
|
||||
expectedError: `undeclared reference to 'test'`,
|
||||
},
|
||||
// TODO: CustomAuditAnnotations: string valueExpression with {oldObject} is allowed
|
||||
}
|
||||
// Include the test library, which includes the test() function in the storage environment during test
|
||||
base := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())
|
||||
extended, err := base.Extend(environment.VersionedOptions{
|
||||
IntroducedVersion: version.MustParseGeneric("1.999"),
|
||||
EnvOptions: []cel.EnvOption{library.Test()},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
compiler = plugincel.NewCompiler(extended)
|
||||
defer func() {
|
||||
compiler = plugincel.NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()))
|
||||
}()
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
errs := ValidateValidatingAdmissionPolicyUpdate(test.config, test.oldconfig)
|
||||
@@ -3088,6 +3321,50 @@ func TestValidateValidatingAdmissionPolicyUpdate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func validatingAdmissionPolicyWithExpressions(
|
||||
matchConditions []admissionregistration.MatchCondition,
|
||||
validations []admissionregistration.Validation,
|
||||
auditAnnotations []admissionregistration.AuditAnnotation) *admissionregistration.ValidatingAdmissionPolicy {
|
||||
return &admissionregistration.ValidatingAdmissionPolicy{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "config",
|
||||
},
|
||||
Spec: admissionregistration.ValidatingAdmissionPolicySpec{
|
||||
MatchConstraints: &admissionregistration.MatchResources{
|
||||
ResourceRules: []admissionregistration.NamedRuleWithOperations{
|
||||
{
|
||||
RuleWithOperations: admissionregistration.RuleWithOperations{
|
||||
Operations: []admissionregistration.OperationType{"*"},
|
||||
Rule: admissionregistration.Rule{
|
||||
APIGroups: []string{"a"},
|
||||
APIVersions: []string{"a"},
|
||||
Resources: []string{"a"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
NamespaceSelector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"a": "b"},
|
||||
},
|
||||
ObjectSelector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"a": "b"},
|
||||
},
|
||||
MatchPolicy: func() *admissionregistration.MatchPolicyType {
|
||||
r := admissionregistration.MatchPolicyType("Exact")
|
||||
return &r
|
||||
}(),
|
||||
},
|
||||
FailurePolicy: func() *admissionregistration.FailurePolicyType {
|
||||
r := admissionregistration.FailurePolicyType("Ignore")
|
||||
return &r
|
||||
}(),
|
||||
MatchConditions: matchConditions,
|
||||
Validations: validations,
|
||||
AuditAnnotations: auditAnnotations,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateValidatingAdmissionPolicyBinding(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -27,6 +27,8 @@ import (
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
celgo "github.com/google/cel-go/cel"
|
||||
|
||||
"k8s.io/apiextensions-apiserver/pkg/apihelpers"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel"
|
||||
structuraldefaulting "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/defaulting"
|
||||
@@ -37,6 +39,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
celconfig "k8s.io/apiserver/pkg/apis/cel"
|
||||
apiservercel "k8s.io/apiserver/pkg/cel"
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
"k8s.io/apiserver/pkg/util/webhook"
|
||||
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||
@@ -81,6 +84,7 @@ func ValidateCustomResourceDefinition(ctx context.Context, obj *apiextensions.Cu
|
||||
requirePrunedDefaults: true,
|
||||
requireAtomicSetType: true,
|
||||
requireMapListKeysMapSetValidation: true,
|
||||
celEnvironmentSet: environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()),
|
||||
}
|
||||
|
||||
allErrs := genericvalidation.ValidateObjectMeta(&obj.ObjectMeta, false, nameValidationFn, field.NewPath("metadata"))
|
||||
@@ -116,6 +120,54 @@ type validationOptions struct {
|
||||
// 1. For x-kubernetes-list-type=map list, key fields are not nullable, and are required or have a default
|
||||
// 2. For x-kubernetes-list-type=map or x-kubernetes-list-type=set list, the whole item must not be nullable.
|
||||
requireMapListKeysMapSetValidation bool
|
||||
// preexistingExpressions tracks which CEL expressions existed in an object before an update. May be nil for create.
|
||||
preexistingExpressions preexistingExpressions
|
||||
|
||||
celEnvironmentSet *environment.EnvSet
|
||||
}
|
||||
|
||||
type preexistingExpressions struct {
|
||||
rules sets.Set[string]
|
||||
messageExpressions sets.Set[string]
|
||||
}
|
||||
|
||||
func (pe preexistingExpressions) RuleEnv(envSet *environment.EnvSet, expression string) *celgo.Env {
|
||||
if pe.rules.Has(expression) {
|
||||
return envSet.StoredExpressionsEnv()
|
||||
}
|
||||
return envSet.NewExpressionsEnv()
|
||||
}
|
||||
|
||||
func (pe preexistingExpressions) MessageExpressionEnv(envSet *environment.EnvSet, expression string) *celgo.Env {
|
||||
if pe.messageExpressions.Has(expression) {
|
||||
return envSet.StoredExpressionsEnv()
|
||||
}
|
||||
return envSet.NewExpressionsEnv()
|
||||
}
|
||||
|
||||
func findPreexistingExpressions(spec *apiextensions.CustomResourceDefinitionSpec) preexistingExpressions {
|
||||
expressions := preexistingExpressions{rules: sets.New[string](), messageExpressions: sets.New[string]()}
|
||||
if spec.Validation != nil && spec.Validation.OpenAPIV3Schema != nil {
|
||||
findPreexistingExpressionsInSchema(spec.Validation.OpenAPIV3Schema, expressions)
|
||||
}
|
||||
for _, v := range spec.Versions {
|
||||
if v.Schema != nil && v.Schema.OpenAPIV3Schema != nil {
|
||||
findPreexistingExpressionsInSchema(v.Schema.OpenAPIV3Schema, expressions)
|
||||
}
|
||||
}
|
||||
return expressions
|
||||
}
|
||||
|
||||
func findPreexistingExpressionsInSchema(schema *apiextensions.JSONSchemaProps, expressions preexistingExpressions) {
|
||||
SchemaHas(schema, func(s *apiextensions.JSONSchemaProps) bool {
|
||||
for _, v := range s.XValidations {
|
||||
expressions.rules.Insert(v.Rule)
|
||||
if len(v.MessageExpression) > 0 {
|
||||
expressions.messageExpressions.Insert(v.Rule)
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
// ValidateCustomResourceDefinitionUpdate statically validates
|
||||
@@ -131,8 +183,13 @@ func ValidateCustomResourceDefinitionUpdate(ctx context.Context, obj, oldObj *ap
|
||||
requirePrunedDefaults: requirePrunedDefaults(&oldObj.Spec),
|
||||
requireAtomicSetType: requireAtomicSetType(&oldObj.Spec),
|
||||
requireMapListKeysMapSetValidation: requireMapListKeysMapSetValidation(&oldObj.Spec),
|
||||
preexistingExpressions: findPreexistingExpressions(&oldObj.Spec),
|
||||
celEnvironmentSet: environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()),
|
||||
}
|
||||
return validateCustomResourceDefinitionUpdate(ctx, obj, oldObj, opts)
|
||||
}
|
||||
|
||||
func validateCustomResourceDefinitionUpdate(ctx context.Context, obj, oldObj *apiextensions.CustomResourceDefinition, opts validationOptions) field.ErrorList {
|
||||
allErrs := genericvalidation.ValidateObjectMetaUpdate(&obj.ObjectMeta, &oldObj.ObjectMeta, field.NewPath("metadata"))
|
||||
allErrs = append(allErrs, validateCustomResourceDefinitionSpecUpdate(ctx, &obj.Spec, &oldObj.Spec, opts, field.NewPath("spec"))...)
|
||||
allErrs = append(allErrs, ValidateCustomResourceDefinitionStatus(&obj.Status, field.NewPath("status"))...)
|
||||
@@ -1000,7 +1057,6 @@ func ValidateCustomResourceDefinitionOpenAPISchema(schema *apiextensions.JSONSch
|
||||
if opts.requireMapListKeysMapSetValidation {
|
||||
allErrs.SchemaErrors = append(allErrs.SchemaErrors, validateMapListKeysMapSet(schema, fldPath)...)
|
||||
}
|
||||
|
||||
if len(schema.XValidations) > 0 {
|
||||
for i, rule := range schema.XValidations {
|
||||
trimmedRule := strings.TrimSpace(rule.Rule)
|
||||
@@ -1031,7 +1087,7 @@ func ValidateCustomResourceDefinitionOpenAPISchema(schema *apiextensions.JSONSch
|
||||
} else if typeInfo == nil {
|
||||
allErrs.CELErrors = append(allErrs.CELErrors, field.InternalError(fldPath.Child("x-kubernetes-validations"), fmt.Errorf("internal error: failed to retrieve type information for x-kubernetes-validations")))
|
||||
} else {
|
||||
compResults, err := cel.Compile(typeInfo.Schema, typeInfo.DeclType, celconfig.PerCallLimit)
|
||||
compResults, err := cel.Compile(typeInfo.Schema, typeInfo.DeclType, celconfig.PerCallLimit, opts.celEnvironmentSet, opts.preexistingExpressions)
|
||||
if err != nil {
|
||||
allErrs.CELErrors = append(allErrs.CELErrors, field.InternalError(fldPath.Child("x-kubernetes-validations"), err))
|
||||
} else {
|
||||
|
||||
@@ -24,6 +24,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/cel-go/cel"
|
||||
|
||||
"k8s.io/utils/pointer"
|
||||
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||
@@ -35,6 +37,9 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||
"k8s.io/apimachinery/pkg/util/json"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/apimachinery/pkg/util/version"
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
"k8s.io/apiserver/pkg/cel/library"
|
||||
)
|
||||
|
||||
type validationMatch struct {
|
||||
@@ -6227,6 +6232,142 @@ func TestValidateCustomResourceDefinitionUpdate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateCustomResourceDefinitionValidationRuleCompatibility(t *testing.T) {
|
||||
allValidationsErrors := []validationMatch{
|
||||
invalid("spec", "validation", "openAPIV3Schema", "properties[x]", "x-kubernetes-validations[0]", "rule"),
|
||||
invalid("spec", "validation", "openAPIV3Schema", "properties[obj]", "x-kubernetes-validations[0]", "rule"),
|
||||
invalid("spec", "validation", "openAPIV3Schema", "properties[obj]", "properties[a]", "x-kubernetes-validations[0]", "rule"),
|
||||
invalid("spec", "validation", "openAPIV3Schema", "properties[array]", "x-kubernetes-validations[0]", "rule"),
|
||||
invalid("spec", "validation", "openAPIV3Schema", "properties[array]", "items", "x-kubernetes-validations[0]", "rule"),
|
||||
invalid("spec", "validation", "openAPIV3Schema", "properties[map]", "x-kubernetes-validations[0]", "rule"),
|
||||
invalid("spec", "validation", "openAPIV3Schema", "properties[map]", "additionalProperties", "x-kubernetes-validations[0]", "rule"),
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
storedRule string
|
||||
updatedRule string
|
||||
errors []validationMatch
|
||||
}{
|
||||
{
|
||||
name: "functions declared for storage mode allowed if expression is unchanged from what is stored",
|
||||
storedRule: "test() == true",
|
||||
updatedRule: "test() == true",
|
||||
},
|
||||
{
|
||||
name: "functions declared for storage mode not allowed if expression is changed",
|
||||
storedRule: "test() == false",
|
||||
updatedRule: "test() == true",
|
||||
errors: allValidationsErrors,
|
||||
},
|
||||
}
|
||||
|
||||
// Include the test library, which includes the test() function in the storage environment during test
|
||||
base := environment.MustBaseEnvSet(version.MajorMinor(1, 998))
|
||||
envSet, err := base.Extend(environment.VersionedOptions{
|
||||
IntroducedVersion: version.MajorMinor(1, 999),
|
||||
EnvOptions: []cel.EnvOption{library.Test()},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
fn := func(rule string) *apiextensions.CustomResourceDefinition {
|
||||
validationRules := []apiextensions.ValidationRule{
|
||||
{
|
||||
Rule: rule,
|
||||
},
|
||||
}
|
||||
return &apiextensions.CustomResourceDefinition{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com", ResourceVersion: "1"},
|
||||
Spec: apiextensions.CustomResourceDefinitionSpec{
|
||||
Group: "group.com",
|
||||
Scope: apiextensions.ResourceScope("Cluster"),
|
||||
Names: apiextensions.CustomResourceDefinitionNames{Plural: "plural", Singular: "singular", Kind: "Plural", ListKind: "PluralList"},
|
||||
Versions: []apiextensions.CustomResourceDefinitionVersion{{Name: "version", Served: true, Storage: true}},
|
||||
Validation: &apiextensions.CustomResourceValidation{
|
||||
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
|
||||
Type: "object",
|
||||
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||
"x": {
|
||||
Type: "string",
|
||||
XValidations: validationRules,
|
||||
},
|
||||
"obj": {
|
||||
Type: "object",
|
||||
Properties: map[string]apiextensions.JSONSchemaProps{
|
||||
"a": {
|
||||
Type: "string",
|
||||
XValidations: validationRules,
|
||||
},
|
||||
},
|
||||
XValidations: validationRules,
|
||||
},
|
||||
"array": {
|
||||
Type: "array",
|
||||
MaxItems: pointer.Int64(1),
|
||||
Items: &apiextensions.JSONSchemaPropsOrArray{
|
||||
Schema: &apiextensions.JSONSchemaProps{
|
||||
Type: "string",
|
||||
XValidations: validationRules,
|
||||
},
|
||||
},
|
||||
XValidations: validationRules,
|
||||
},
|
||||
"map": {
|
||||
Type: "object",
|
||||
MaxProperties: pointer.Int64(1),
|
||||
AdditionalProperties: &apiextensions.JSONSchemaPropsOrBool{
|
||||
Schema: &apiextensions.JSONSchemaProps{
|
||||
Type: "string",
|
||||
XValidations: validationRules,
|
||||
},
|
||||
},
|
||||
XValidations: validationRules,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: apiextensions.CustomResourceDefinitionStatus{StoredVersions: []string{"version"}},
|
||||
}
|
||||
}
|
||||
old := fn(tc.storedRule)
|
||||
resource := fn(tc.updatedRule)
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
errs := validateCustomResourceDefinitionUpdate(ctx, resource, old, validationOptions{
|
||||
preexistingExpressions: findPreexistingExpressions(&old.Spec),
|
||||
celEnvironmentSet: envSet,
|
||||
})
|
||||
seenErrs := make([]bool, len(errs))
|
||||
|
||||
for _, expectedError := range tc.errors {
|
||||
found := false
|
||||
for i, err := range errs {
|
||||
if expectedError.matches(err) && !seenErrs[i] {
|
||||
found = true
|
||||
seenErrs[i] = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Errorf("expected %v at %v, got %v", expectedError.errorType, expectedError.path.String(), errs)
|
||||
}
|
||||
}
|
||||
|
||||
for i, seen := range seenErrs {
|
||||
if !seen {
|
||||
t.Errorf("unexpected error: %v", errs[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateCustomResourceDefinitionValidation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -8736,6 +8877,9 @@ func TestValidateCustomResourceDefinitionValidation(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
if tt.opts.celEnvironmentSet == nil {
|
||||
tt.opts.celEnvironmentSet = environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())
|
||||
}
|
||||
got := validateCustomResourceDefinitionValidation(ctx, &tt.input, tt.statusEnabled, tt.opts, field.NewPath("spec", "validation"))
|
||||
|
||||
seenErrs := make([]bool, len(got))
|
||||
@@ -9170,7 +9314,9 @@ func TestCelContext(t *testing.T) {
|
||||
}
|
||||
celContext := RootCELContext(tt.schema)
|
||||
celContext.converter = converter
|
||||
opts := validationOptions{}
|
||||
opts := validationOptions{
|
||||
celEnvironmentSet: environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()),
|
||||
}
|
||||
openAPIV3Schema := &specStandardValidatorV3{
|
||||
allowDefaults: opts.allowDefaults,
|
||||
disallowDefaultsReason: opts.disallowDefaultsReason,
|
||||
|
||||
@@ -19,7 +19,6 @@ package cel
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/cel-go/cel"
|
||||
@@ -27,8 +26,10 @@ import (
|
||||
|
||||
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"
|
||||
)
|
||||
@@ -66,27 +67,41 @@ type CompilationResult struct {
|
||||
MessageExpressionMaxCost uint64
|
||||
}
|
||||
|
||||
var (
|
||||
initEnvOnce sync.Once
|
||||
initEnv *cel.Env
|
||||
initEnvErr error
|
||||
)
|
||||
// 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
|
||||
}
|
||||
|
||||
// This func is duplicated in k8s.io/apiserver/pkg/admission/plugin/cel/validator.go
|
||||
// If any changes are made here, consider to make the same changes there as well.
|
||||
func getBaseEnv() (*cel.Env, error) {
|
||||
initEnvOnce.Do(func() {
|
||||
var opts []cel.EnvOption
|
||||
opts = append(opts, cel.HomogeneousAggregateLiterals())
|
||||
// Validate function declarations once during base env initialization,
|
||||
// so they don't need to be evaluated each time a CEL rule is compiled.
|
||||
// This is a relatively expensive operation.
|
||||
opts = append(opts, cel.EagerlyValidateDeclarations(true), cel.DefaultUTCTimeZone(true))
|
||||
opts = append(opts, library.ExtensionLibs...)
|
||||
// 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()
|
||||
}}
|
||||
}
|
||||
|
||||
initEnv, initEnvErr = cel.NewEnv(opts...)
|
||||
})
|
||||
return initEnv, initEnvErr
|
||||
// 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
|
||||
@@ -98,7 +113,8 @@ func getBaseEnv() (*cel.Env, 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.
|
||||
func Compile(s *schema.Structural, declType *apiservercel.DeclType, perCallLimit uint64) ([]CompilationResult, error) {
|
||||
// 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))
|
||||
@@ -109,58 +125,52 @@ func Compile(s *schema.Structural, declType *apiservercel.DeclType, perCallLimit
|
||||
}
|
||||
celRules := s.Extensions.XValidations
|
||||
|
||||
var propDecls []cel.EnvOption
|
||||
var root *apiservercel.DeclType
|
||||
var ok bool
|
||||
baseEnv, err := getBaseEnv()
|
||||
envSet, err := prepareEnvSet(baseEnvSet, declType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reg := apiservercel.NewRegistry(baseEnv)
|
||||
scopedTypeName := generateUniqueSelfTypeName()
|
||||
rt, err := apiservercel.NewRuleTypes(scopedTypeName, declType, reg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if rt == nil {
|
||||
return nil, nil
|
||||
}
|
||||
opts, err := rt.EnvOptions(baseEnv.TypeProvider())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
root, ok = rt.FindDeclType(scopedTypeName)
|
||||
if !ok {
|
||||
if declType == nil {
|
||||
return nil, fmt.Errorf("rule declared on schema that does not support validation rules type: '%s' x-kubernetes-preserve-unknown-fields: '%t'", s.Type, s.XPreserveUnknownFields)
|
||||
}
|
||||
root = declType.MaybeAssignTypeName(scopedTypeName)
|
||||
}
|
||||
propDecls = append(propDecls, cel.Variable(ScopedVarName, root.CelType()))
|
||||
propDecls = append(propDecls, cel.Variable(OldScopedVarName, root.CelType()))
|
||||
opts = append(opts, propDecls...)
|
||||
env, err := baseEnv.Extend(opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
estimator := newCostEstimator(root)
|
||||
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(root.MinSerializedSize)
|
||||
maxCardinality := maxCardinality(declType.MinSerializedSize)
|
||||
for i, rule := range celRules {
|
||||
compResults[i] = compileRule(rule, env, perCallLimit, estimator, maxCardinality)
|
||||
compResults[i] = compileRule(rule, envSet, envLoader, estimator, maxCardinality, perCallLimit)
|
||||
}
|
||||
|
||||
return compResults, nil
|
||||
}
|
||||
|
||||
func compileRule(rule apiextensions.ValidationRule, env *cel.Env, perCallLimit uint64, estimator *library.CostEstimator, maxCardinality uint64) (compilationResult CompilationResult) {
|
||||
func prepareEnvSet(baseEnvSet *environment.EnvSet, declType *apiservercel.DeclType) (*environment.EnvSet, error) {
|
||||
scopedType := declType.MaybeAssignTypeName(generateUniqueSelfTypeName())
|
||||
return 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()),
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func compileRule(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
|
||||
}
|
||||
ast, issues := env.Compile(rule.Rule)
|
||||
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
|
||||
@@ -184,18 +194,16 @@ func compileRule(rule apiextensions.ValidationRule, env *cel.Env, perCallLimit u
|
||||
}
|
||||
|
||||
// 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 := env.Program(ast,
|
||||
cel.EvalOptions(cel.OptOptimize, cel.OptTrackCost),
|
||||
prog, err := ruleEnv.Program(ast,
|
||||
cel.CostLimit(perCallLimit),
|
||||
cel.CostTracking(estimator),
|
||||
cel.OptimizeRegex(library.ExtensionLibRegexOptimizations...),
|
||||
cel.InterruptCheckFrequency(celconfig.CheckFrequency),
|
||||
)
|
||||
if err != nil {
|
||||
compilationResult.Error = &apiservercel.Error{Type: apiservercel.ErrorTypeInvalid, Detail: "program instantiation failed: " + err.Error()}
|
||||
return
|
||||
}
|
||||
costEst, err := env.EstimateCost(ast, estimator)
|
||||
costEst, err := ruleEnv.EstimateCost(ast, estimator)
|
||||
if err != nil {
|
||||
compilationResult.Error = &apiservercel.Error{Type: apiservercel.ErrorTypeInternal, Detail: "cost estimation failed: " + err.Error()}
|
||||
return
|
||||
@@ -204,7 +212,8 @@ func compileRule(rule apiextensions.ValidationRule, env *cel.Env, perCallLimit u
|
||||
compilationResult.MaxCardinality = maxCardinality
|
||||
compilationResult.Program = prog
|
||||
if rule.MessageExpression != "" {
|
||||
ast, issues := env.Compile(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
|
||||
@@ -220,18 +229,16 @@ func compileRule(rule apiextensions.ValidationRule, env *cel.Env, perCallLimit u
|
||||
return
|
||||
}
|
||||
|
||||
msgProg, err := env.Program(ast,
|
||||
cel.EvalOptions(cel.OptOptimize, cel.OptTrackCost),
|
||||
msgProg, err := messageEnv.Program(ast,
|
||||
cel.CostLimit(perCallLimit),
|
||||
cel.CostTracking(estimator),
|
||||
cel.OptimizeRegex(library.ExtensionLibRegexOptimizations...),
|
||||
cel.InterruptCheckFrequency(celconfig.CheckFrequency),
|
||||
)
|
||||
if err != nil {
|
||||
compilationResult.MessageExpressionError = &apiservercel.Error{Type: apiservercel.ErrorTypeInvalid, Detail: "messageExpression instantiation failed: " + err.Error()}
|
||||
return
|
||||
}
|
||||
costEst, err := env.EstimateCost(ast, estimator)
|
||||
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
|
||||
|
||||
@@ -22,12 +22,17 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apiserver/pkg/cel"
|
||||
celgo "github.com/google/cel-go/cel"
|
||||
"github.com/google/cel-go/common/types"
|
||||
"github.com/google/cel-go/common/types/ref"
|
||||
|
||||
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/apimachinery/pkg/util/version"
|
||||
celconfig "k8s.io/apiserver/pkg/apis/cel"
|
||||
"k8s.io/apiserver/pkg/cel"
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -150,6 +155,7 @@ func TestCelCompilation(t *testing.T) {
|
||||
name string
|
||||
input schema.Structural
|
||||
expectedResults []validationMatcher
|
||||
unmodified bool
|
||||
}{
|
||||
{
|
||||
name: "valid object",
|
||||
@@ -715,13 +721,59 @@ func TestCelCompilation(t *testing.T) {
|
||||
messageExpressionError("messageExpression compilation failed"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unmodified expression may use CEL environment features planned to be added in future releases",
|
||||
input: schema.Structural{
|
||||
Generic: schema.Generic{
|
||||
Type: "object",
|
||||
},
|
||||
Extensions: schema.Extensions{
|
||||
XValidations: apiextensions.ValidationRules{
|
||||
{Rule: "fakeFunction('abc') == 'ABC'"},
|
||||
},
|
||||
},
|
||||
},
|
||||
unmodified: true,
|
||||
expectedResults: []validationMatcher{
|
||||
noError(),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "modified expressions may not use CEL environment features planned to be added in future releases",
|
||||
input: schema.Structural{
|
||||
Generic: schema.Generic{
|
||||
Type: "object",
|
||||
},
|
||||
Extensions: schema.Extensions{
|
||||
XValidations: apiextensions.ValidationRules{
|
||||
{Rule: "fakeFunction('abc') == 'ABC'"},
|
||||
},
|
||||
},
|
||||
},
|
||||
unmodified: false,
|
||||
expectedResults: []validationMatcher{
|
||||
invalidError("undeclared reference to 'fakeFunction'"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
compilationResults, err := Compile(&tt.input, model.SchemaDeclType(&tt.input, false), celconfig.PerCallLimit)
|
||||
env, err := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()).Extend(
|
||||
environment.VersionedOptions{
|
||||
IntroducedVersion: version.MajorMinor(1, 999),
|
||||
EnvOptions: []celgo.EnvOption{celgo.Lib(&fakeLib{})},
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, but got: %v", err)
|
||||
t.Fatal(err)
|
||||
}
|
||||
loader := NewExpressionsEnvLoader()
|
||||
if tt.unmodified {
|
||||
loader = StoredExpressionsEnvLoader()
|
||||
}
|
||||
compilationResults, err := Compile(&tt.input, model.SchemaDeclType(&tt.input, false), celconfig.PerCallLimit, env, loader)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, but got: %v", err)
|
||||
}
|
||||
|
||||
if len(compilationResults) != len(tt.input.XValidations) {
|
||||
@@ -1155,12 +1207,12 @@ func genMapWithCustomItemRule(item *schema.Structural, rule string) func(maxProp
|
||||
// if expectedCostExceedsLimit is non-zero. Typically, only expectedCost or expectedCostExceedsLimit is non-zero, not both.
|
||||
func schemaChecker(schema *schema.Structural, expectedCost uint64, expectedCostExceedsLimit uint64, t *testing.T) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
compilationResults, err := Compile(schema, model.SchemaDeclType(schema, false), celconfig.PerCallLimit)
|
||||
compilationResults, err := Compile(schema, model.SchemaDeclType(schema, false), celconfig.PerCallLimit, environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()), NewExpressionsEnvLoader())
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got: %v", err)
|
||||
t.Fatalf("Expected no error, got: %v", err)
|
||||
}
|
||||
if len(compilationResults) != 1 {
|
||||
t.Errorf("Expected one rule, got: %d", len(compilationResults))
|
||||
t.Fatalf("Expected one rule, got: %d", len(compilationResults))
|
||||
}
|
||||
result := compilationResults[0]
|
||||
if result.Error != nil {
|
||||
@@ -1704,17 +1756,43 @@ func TestCostEstimation(t *testing.T) {
|
||||
}
|
||||
|
||||
func BenchmarkCompile(b *testing.B) {
|
||||
_, err := getBaseEnv() // prime the baseEnv
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
env := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()) // prepare the environment
|
||||
s := genArrayWithRule("number", "true")(nil)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := Compile(s, model.SchemaDeclType(s, false), uint64(math.MaxInt64))
|
||||
_, err := Compile(s, model.SchemaDeclType(s, false), math.MaxInt64, env, NewExpressionsEnvLoader())
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type fakeLib struct{}
|
||||
|
||||
var testLibraryDecls = map[string][]celgo.FunctionOpt{
|
||||
"fakeFunction": {
|
||||
celgo.Overload("fakeFunction", []*celgo.Type{celgo.StringType}, celgo.StringType,
|
||||
celgo.UnaryBinding(fakeFunction))},
|
||||
}
|
||||
|
||||
func (*fakeLib) CompileOptions() []celgo.EnvOption {
|
||||
options := make([]celgo.EnvOption, 0, len(testLibraryDecls))
|
||||
for name, overloads := range testLibraryDecls {
|
||||
options = append(options, celgo.Function(name, overloads...))
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
func (*fakeLib) ProgramOptions() []celgo.ProgramOption {
|
||||
return []celgo.ProgramOption{}
|
||||
}
|
||||
|
||||
func fakeFunction(arg1 ref.Val) ref.Val {
|
||||
arg, ok := arg1.Value().(string)
|
||||
if !ok {
|
||||
return types.MaybeNoSuchOverloadErr(arg1)
|
||||
}
|
||||
|
||||
return types.String(strings.ToUpper(arg))
|
||||
}
|
||||
|
||||
@@ -29,12 +29,7 @@ import (
|
||||
|
||||
func TestTypes_RuleTypesFieldMapping(t *testing.T) {
|
||||
stdEnv, _ := cel.NewEnv()
|
||||
reg := apiservercel.NewRegistry(stdEnv)
|
||||
rt, err := apiservercel.NewRuleTypes("CustomObject", SchemaDeclType(testSchema(), true), reg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rt.TypeProvider = stdEnv.TypeProvider()
|
||||
rt := apiservercel.NewDeclTypeProvider(SchemaDeclType(testSchema(), true).MaybeAssignTypeName("CustomObject"))
|
||||
nestedFieldType, found := rt.FindFieldType("CustomObject", "nested")
|
||||
if !found {
|
||||
t.Fatal("got field not found for 'CustomObject.nested', wanted found")
|
||||
|
||||
@@ -30,13 +30,15 @@ import (
|
||||
"github.com/google/cel-go/common/types/ref"
|
||||
"github.com/google/cel-go/interpreter"
|
||||
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
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/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/apiserver/pkg/cel"
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
"k8s.io/apiserver/pkg/cel/metrics"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
celconfig "k8s.io/apiserver/pkg/apis/cel"
|
||||
)
|
||||
@@ -82,7 +84,7 @@ func NewValidator(s *schema.Structural, isResourceRoot bool, perCallLimit uint64
|
||||
// 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)
|
||||
compiledRules, err := Compile(s, declType, perCallLimit, environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()), StoredExpressionsEnvLoader())
|
||||
var itemsValidator, additionalPropertiesValidator *Validator
|
||||
var propertiesValidators map[string]Validator
|
||||
if s.Items != nil {
|
||||
|
||||
@@ -40,6 +40,7 @@ func TestValidationExpressions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
schema *schema.Structural
|
||||
oldSchema *schema.Structural
|
||||
obj interface{}
|
||||
oldObj interface{}
|
||||
valid []string
|
||||
@@ -173,6 +174,60 @@ func TestValidationExpressions(t *testing.T) {
|
||||
"double(self.val1) < 10.0",
|
||||
"double(self.val2) == 10.0",
|
||||
"double(self.val3) > 10.0",
|
||||
|
||||
// Cross Type Numeric Comparisons: integers with all float types
|
||||
"self.val1 < self.val4",
|
||||
"self.val1 <= self.val4",
|
||||
"self.val2 <= self.val4",
|
||||
"self.val2 >= self.val4",
|
||||
"self.val3 > self.val4",
|
||||
"self.val3 >= self.val4",
|
||||
|
||||
"self.val1 < self.val4",
|
||||
"self.val3 > self.val4",
|
||||
|
||||
"self.val1 < self.val5",
|
||||
"self.val3 > self.val5",
|
||||
|
||||
"self.val1 < self.val5",
|
||||
"self.val3 > self.val5",
|
||||
|
||||
"self.val1 < self.val6",
|
||||
"self.val3 > self.val6",
|
||||
|
||||
"self.val1 < self.val6",
|
||||
"self.val3 > self.val6",
|
||||
|
||||
// Cross Type Numeric Comparisons: float types backed by integer values,
|
||||
// which is how integer literals are parsed from JSON for custom resources.
|
||||
"self.val1 < self.val7",
|
||||
"self.val3 > self.val7",
|
||||
|
||||
"self.val1 < int(self.val7)",
|
||||
"self.val3 > int(self.val7)",
|
||||
|
||||
"self.val1 < self.val8",
|
||||
"self.val3 > self.val8",
|
||||
|
||||
"self.val1 < self.val8",
|
||||
"self.val3 > self.val8",
|
||||
|
||||
"self.val1 < self.val9",
|
||||
"self.val3 > self.val9",
|
||||
|
||||
"self.val1 < self.val9",
|
||||
"self.val3 > self.val9",
|
||||
|
||||
// Cross Type Numeric Comparisons: literal integers and floats
|
||||
"5 < 10.0",
|
||||
"15 > 10.0",
|
||||
|
||||
"5 < 10.0",
|
||||
"15 > 10.0",
|
||||
|
||||
// Cross Type Numeric Comparisons: integers with literal floats
|
||||
"self.val1 < 10.0",
|
||||
"self.val3 > 10.0",
|
||||
},
|
||||
},
|
||||
{name: "unicode strings",
|
||||
@@ -1784,7 +1839,7 @@ func TestValidationExpressions(t *testing.T) {
|
||||
oldObj: []interface{}{},
|
||||
schema: objectTypePtr(map[string]schema.Structural{}),
|
||||
errors: map[string]string{
|
||||
"authorizer.path('/healthz').check('get').isAllowed()": "undeclared reference to 'authorizer'",
|
||||
"authorizer.path('/healthz').check('get').allowed()": "undeclared reference to 'authorizer'",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
|
||||
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation"
|
||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
@@ -33,7 +35,6 @@ import (
|
||||
"k8s.io/apiserver/pkg/storage"
|
||||
"k8s.io/apiserver/pkg/storage/names"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
|
||||
)
|
||||
|
||||
// strategy implements behavior for CustomResources.
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation"
|
||||
|
||||
@@ -121,6 +121,11 @@ func MustParseSemantic(str string) *Version {
|
||||
return v
|
||||
}
|
||||
|
||||
// MajorMinor returns a version with the provided major and minor version.
|
||||
func MajorMinor(major, minor uint) *Version {
|
||||
return &Version{components: []uint{major, minor}}
|
||||
}
|
||||
|
||||
// Major returns the major release number
|
||||
func (v *Version) Major() uint {
|
||||
return v.components[0]
|
||||
|
||||
@@ -18,12 +18,13 @@ package cel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
celconfig "k8s.io/apiserver/pkg/apis/cel"
|
||||
"sync"
|
||||
|
||||
"github.com/google/cel-go/cel"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -36,104 +37,6 @@ const (
|
||||
RequestResourceAuthorizerVarName = "authorizer.requestResource"
|
||||
)
|
||||
|
||||
var (
|
||||
initEnvsOnce sync.Once
|
||||
initEnvs envs
|
||||
initEnvsErr error
|
||||
)
|
||||
|
||||
func getEnvs() (envs, error) {
|
||||
initEnvsOnce.Do(func() {
|
||||
requiredVarsEnv, err := buildRequiredVarsEnv()
|
||||
if err != nil {
|
||||
initEnvsErr = err
|
||||
return
|
||||
}
|
||||
|
||||
initEnvs, err = buildWithOptionalVarsEnvs(requiredVarsEnv)
|
||||
if err != nil {
|
||||
initEnvsErr = err
|
||||
return
|
||||
}
|
||||
})
|
||||
return initEnvs, initEnvsErr
|
||||
}
|
||||
|
||||
// This is a similar code as in k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation.go
|
||||
// If any changes are made here, consider to make the same changes there as well.
|
||||
func buildBaseEnv() (*cel.Env, error) {
|
||||
var opts []cel.EnvOption
|
||||
opts = append(opts, cel.HomogeneousAggregateLiterals())
|
||||
// Validate function declarations once during base env initialization,
|
||||
// so they don't need to be evaluated each time a CEL rule is compiled.
|
||||
// This is a relatively expensive operation.
|
||||
opts = append(opts, cel.EagerlyValidateDeclarations(true), cel.DefaultUTCTimeZone(true))
|
||||
opts = append(opts, library.ExtensionLibs...)
|
||||
|
||||
return cel.NewEnv(opts...)
|
||||
}
|
||||
|
||||
func buildRequiredVarsEnv() (*cel.Env, error) {
|
||||
baseEnv, err := buildBaseEnv()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var propDecls []cel.EnvOption
|
||||
reg := apiservercel.NewRegistry(baseEnv)
|
||||
|
||||
requestType := BuildRequestType()
|
||||
rt, err := apiservercel.NewRuleTypes(requestType.TypeName(), requestType, reg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if rt == nil {
|
||||
return nil, nil
|
||||
}
|
||||
opts, err := rt.EnvOptions(baseEnv.TypeProvider())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
propDecls = append(propDecls, cel.Variable(ObjectVarName, cel.DynType))
|
||||
propDecls = append(propDecls, cel.Variable(OldObjectVarName, cel.DynType))
|
||||
propDecls = append(propDecls, cel.Variable(RequestVarName, requestType.CelType()))
|
||||
|
||||
opts = append(opts, propDecls...)
|
||||
env, err := baseEnv.Extend(opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return env, nil
|
||||
}
|
||||
|
||||
type envs map[OptionalVariableDeclarations]*cel.Env
|
||||
|
||||
func buildEnvWithVars(baseVarsEnv *cel.Env, options OptionalVariableDeclarations) (*cel.Env, error) {
|
||||
var opts []cel.EnvOption
|
||||
if options.HasParams {
|
||||
opts = append(opts, cel.Variable(ParamsVarName, cel.DynType))
|
||||
}
|
||||
if options.HasAuthorizer {
|
||||
opts = append(opts, cel.Variable(AuthorizerVarName, library.AuthorizerType))
|
||||
opts = append(opts, cel.Variable(RequestResourceAuthorizerVarName, library.ResourceCheckType))
|
||||
}
|
||||
return baseVarsEnv.Extend(opts...)
|
||||
}
|
||||
|
||||
func buildWithOptionalVarsEnvs(requiredVarsEnv *cel.Env) (envs, error) {
|
||||
envs := make(envs, 4) // since the number of variable combinations is small, pre-build a environment for each
|
||||
for _, hasParams := range []bool{false, true} {
|
||||
for _, hasAuthorizer := range []bool{false, true} {
|
||||
opts := OptionalVariableDeclarations{HasParams: hasParams, HasAuthorizer: hasAuthorizer}
|
||||
env, err := buildEnvWithVars(requiredVarsEnv, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
envs[opts] = env
|
||||
}
|
||||
}
|
||||
return envs, nil
|
||||
}
|
||||
|
||||
// BuildRequestType generates a DeclType for AdmissionRequest. This may be replaced with a utility that
|
||||
// converts the native type definition to apiservercel.DeclType once such a utility becomes available.
|
||||
// The 'uid' field is omitted since it is not needed for in-process admission review.
|
||||
@@ -188,40 +91,43 @@ type CompilationResult struct {
|
||||
ExpressionAccessor ExpressionAccessor
|
||||
}
|
||||
|
||||
// Compiler provides a CEL expression compiler configured with the desired admission related CEL variables and
|
||||
// environment mode.
|
||||
type Compiler interface {
|
||||
CompileCELExpression(expressionAccessor ExpressionAccessor, options OptionalVariableDeclarations, mode environment.Type) CompilationResult
|
||||
}
|
||||
|
||||
type compiler struct {
|
||||
varEnvs variableDeclEnvs
|
||||
}
|
||||
|
||||
func NewCompiler(env *environment.EnvSet) Compiler {
|
||||
return &compiler{varEnvs: mustBuildEnvs(env)}
|
||||
}
|
||||
|
||||
type variableDeclEnvs map[OptionalVariableDeclarations]*environment.EnvSet
|
||||
|
||||
// CompileCELExpression returns a compiled CEL expression.
|
||||
// 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 CompileCELExpression(expressionAccessor ExpressionAccessor, optionalVars OptionalVariableDeclarations, perCallLimit uint64) CompilationResult {
|
||||
var env *cel.Env
|
||||
envs, err := getEnvs()
|
||||
if err != nil {
|
||||
func (c compiler) CompileCELExpression(expressionAccessor ExpressionAccessor, options OptionalVariableDeclarations, envType environment.Type) CompilationResult {
|
||||
resultError := func(errorString string, errType apiservercel.ErrorType) CompilationResult {
|
||||
return CompilationResult{
|
||||
Error: &apiservercel.Error{
|
||||
Type: apiservercel.ErrorTypeInternal,
|
||||
Detail: "compiler initialization failed: " + err.Error(),
|
||||
},
|
||||
ExpressionAccessor: expressionAccessor,
|
||||
}
|
||||
}
|
||||
env, ok := envs[optionalVars]
|
||||
if !ok {
|
||||
return CompilationResult{
|
||||
Error: &apiservercel.Error{
|
||||
Type: apiservercel.ErrorTypeInvalid,
|
||||
Detail: fmt.Sprintf("compiler initialization failed: failed to load environment for %v", optionalVars),
|
||||
Type: errType,
|
||||
Detail: errorString,
|
||||
},
|
||||
ExpressionAccessor: expressionAccessor,
|
||||
}
|
||||
}
|
||||
|
||||
env, err := c.varEnvs[options].Env(envType)
|
||||
if err != nil {
|
||||
return resultError(fmt.Sprintf("unexpected error loading CEL environment: %v", err), apiservercel.ErrorTypeInternal)
|
||||
}
|
||||
|
||||
ast, issues := env.Compile(expressionAccessor.GetExpression())
|
||||
if issues != nil {
|
||||
return CompilationResult{
|
||||
Error: &apiservercel.Error{
|
||||
Type: apiservercel.ErrorTypeInvalid,
|
||||
Detail: "compilation failed: " + issues.String(),
|
||||
},
|
||||
ExpressionAccessor: expressionAccessor,
|
||||
}
|
||||
return resultError("compilation failed: "+issues.String(), apiservercel.ErrorTypeInvalid)
|
||||
}
|
||||
found := false
|
||||
returnTypes := expressionAccessor.ReturnTypes()
|
||||
@@ -239,43 +145,61 @@ func CompileCELExpression(expressionAccessor ExpressionAccessor, optionalVars Op
|
||||
reason = fmt.Sprintf("must evaluate to one of %v", returnTypes)
|
||||
}
|
||||
|
||||
return CompilationResult{
|
||||
Error: &apiservercel.Error{
|
||||
Type: apiservercel.ErrorTypeInvalid,
|
||||
Detail: reason,
|
||||
},
|
||||
ExpressionAccessor: expressionAccessor,
|
||||
}
|
||||
return resultError(reason, apiservercel.ErrorTypeInvalid)
|
||||
}
|
||||
|
||||
_, err = cel.AstToCheckedExpr(ast)
|
||||
if err != nil {
|
||||
// should be impossible since env.Compile returned no issues
|
||||
return CompilationResult{
|
||||
Error: &apiservercel.Error{
|
||||
Type: apiservercel.ErrorTypeInternal,
|
||||
Detail: "unexpected compilation error: " + err.Error(),
|
||||
},
|
||||
ExpressionAccessor: expressionAccessor,
|
||||
}
|
||||
return resultError("unexpected compilation error: "+err.Error(), apiservercel.ErrorTypeInternal)
|
||||
}
|
||||
prog, err := env.Program(ast,
|
||||
cel.EvalOptions(cel.OptOptimize, cel.OptTrackCost),
|
||||
cel.OptimizeRegex(library.ExtensionLibRegexOptimizations...),
|
||||
cel.InterruptCheckFrequency(celconfig.CheckFrequency),
|
||||
cel.CostLimit(perCallLimit),
|
||||
)
|
||||
if err != nil {
|
||||
return CompilationResult{
|
||||
Error: &apiservercel.Error{
|
||||
Type: apiservercel.ErrorTypeInvalid,
|
||||
Detail: "program instantiation failed: " + err.Error(),
|
||||
},
|
||||
ExpressionAccessor: expressionAccessor,
|
||||
}
|
||||
return resultError("program instantiation failed: "+err.Error(), apiservercel.ErrorTypeInternal)
|
||||
}
|
||||
return CompilationResult{
|
||||
Program: prog,
|
||||
ExpressionAccessor: expressionAccessor,
|
||||
}
|
||||
}
|
||||
|
||||
func mustBuildEnvs(baseEnv *environment.EnvSet) variableDeclEnvs {
|
||||
requestType := BuildRequestType()
|
||||
envs := make(variableDeclEnvs, 4) // since the number of variable combinations is small, pre-build a environment for each
|
||||
for _, hasParams := range []bool{false, true} {
|
||||
for _, hasAuthorizer := range []bool{false, true} {
|
||||
var envOpts []cel.EnvOption
|
||||
if hasParams {
|
||||
envOpts = append(envOpts, cel.Variable(ParamsVarName, cel.DynType))
|
||||
}
|
||||
if hasAuthorizer {
|
||||
envOpts = append(envOpts,
|
||||
cel.Variable(AuthorizerVarName, library.AuthorizerType),
|
||||
cel.Variable(RequestResourceAuthorizerVarName, library.ResourceCheckType))
|
||||
}
|
||||
envOpts = append(envOpts,
|
||||
cel.Variable(ObjectVarName, cel.DynType),
|
||||
cel.Variable(OldObjectVarName, cel.DynType),
|
||||
cel.Variable(RequestVarName, requestType.CelType()))
|
||||
|
||||
extended, err := baseEnv.Extend(
|
||||
environment.VersionedOptions{
|
||||
// Feature epoch was actually 1.26, but we artificially set it to 1.0 because these
|
||||
// options should always be present.
|
||||
IntroducedVersion: version.MajorMinor(1, 0),
|
||||
EnvOptions: envOpts,
|
||||
DeclTypes: []*apiservercel.DeclType{
|
||||
requestType,
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("environment misconfigured: %v", err))
|
||||
}
|
||||
envs[OptionalVariableDeclarations{HasParams: hasParams, HasAuthorizer: hasAuthorizer}] = extended
|
||||
}
|
||||
}
|
||||
return envs
|
||||
}
|
||||
|
||||
@@ -17,12 +17,15 @@ limitations under the License.
|
||||
package cel
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
celgo "github.com/google/cel-go/cel"
|
||||
|
||||
celconfig "k8s.io/apiserver/pkg/apis/cel"
|
||||
"k8s.io/apimachinery/pkg/util/version"
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
"k8s.io/apiserver/pkg/cel/library"
|
||||
)
|
||||
|
||||
func TestCompileValidatingPolicyExpression(t *testing.T) {
|
||||
@@ -32,6 +35,7 @@ func TestCompileValidatingPolicyExpression(t *testing.T) {
|
||||
hasParams bool
|
||||
hasAuthorizer bool
|
||||
errorExpressions map[string]string
|
||||
envType environment.Type
|
||||
}{
|
||||
{
|
||||
name: "invalid syntax",
|
||||
@@ -117,25 +121,57 @@ func TestCompileValidatingPolicyExpression(t *testing.T) {
|
||||
"authorizer.group('') != null": "undeclared reference to 'authorizer'",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "compile with storage environment should recognize functions available only in the storage environment",
|
||||
expressions: []string{
|
||||
"test() == true",
|
||||
},
|
||||
envType: environment.StoredExpressions,
|
||||
},
|
||||
{
|
||||
name: "compile with supported environment should not recognize functions available only in the storage environment",
|
||||
errorExpressions: map[string]string{
|
||||
"test() == true": "undeclared reference to 'test'",
|
||||
},
|
||||
envType: environment.NewExpressions,
|
||||
},
|
||||
}
|
||||
|
||||
// Include the test library, which includes the test() function in the storage environment during test
|
||||
base := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())
|
||||
extended, err := base.Extend(environment.VersionedOptions{
|
||||
IntroducedVersion: version.MajorMinor(1, 999),
|
||||
EnvOptions: []celgo.EnvOption{library.Test()},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
compiler := NewCompiler(extended)
|
||||
|
||||
for _, tc := range cases {
|
||||
envType := tc.envType
|
||||
if envType == "" {
|
||||
envType = environment.NewExpressions
|
||||
}
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
for _, expr := range tc.expressions {
|
||||
t.Run(expr, func(t *testing.T) {
|
||||
t.Run("expression", func(t *testing.T) {
|
||||
result := CompileCELExpression(&fakeValidationCondition{
|
||||
options := OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: tc.hasAuthorizer}
|
||||
|
||||
result := compiler.CompileCELExpression(&fakeValidationCondition{
|
||||
Expression: expr,
|
||||
}, OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: tc.hasAuthorizer}, celconfig.PerCallLimit)
|
||||
}, options, envType)
|
||||
if result.Error != nil {
|
||||
t.Errorf("Unexpected error: %v", result.Error)
|
||||
}
|
||||
})
|
||||
t.Run("auditAnnotation.valueExpression", func(t *testing.T) {
|
||||
// Test audit annotation compilation by casting the result to a string
|
||||
result := CompileCELExpression(&fakeAuditAnnotationCondition{
|
||||
options := OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: tc.hasAuthorizer}
|
||||
result := compiler.CompileCELExpression(&fakeAuditAnnotationCondition{
|
||||
ValueExpression: "string(" + expr + ")",
|
||||
}, OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: tc.hasAuthorizer}, celconfig.PerCallLimit)
|
||||
}, options, envType)
|
||||
if result.Error != nil {
|
||||
t.Errorf("Unexpected error: %v", result.Error)
|
||||
}
|
||||
@@ -145,9 +181,10 @@ func TestCompileValidatingPolicyExpression(t *testing.T) {
|
||||
for expr, expectErr := range tc.errorExpressions {
|
||||
t.Run(expr, func(t *testing.T) {
|
||||
t.Run("expression", func(t *testing.T) {
|
||||
result := CompileCELExpression(&fakeValidationCondition{
|
||||
options := OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: tc.hasAuthorizer}
|
||||
result := compiler.CompileCELExpression(&fakeValidationCondition{
|
||||
Expression: expr,
|
||||
}, OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: tc.hasAuthorizer}, celconfig.PerCallLimit)
|
||||
}, options, envType)
|
||||
if result.Error == nil {
|
||||
t.Errorf("Expected expression '%s' to contain '%v' but got no error", expr, expectErr)
|
||||
return
|
||||
@@ -158,9 +195,10 @@ func TestCompileValidatingPolicyExpression(t *testing.T) {
|
||||
})
|
||||
t.Run("auditAnnotation.valueExpression", func(t *testing.T) {
|
||||
// Test audit annotation compilation by casting the result to a string
|
||||
result := CompileCELExpression(&fakeAuditAnnotationCondition{
|
||||
options := OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: tc.hasAuthorizer}
|
||||
result := compiler.CompileCELExpression(&fakeAuditAnnotationCondition{
|
||||
ValueExpression: "string(" + expr + ")",
|
||||
}, OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: tc.hasAuthorizer}, celconfig.PerCallLimit)
|
||||
}, options, envType)
|
||||
if result.Error == nil {
|
||||
t.Errorf("Expected expression '%s' to contain '%v' but got no error", expr, expectErr)
|
||||
return
|
||||
@@ -175,6 +213,21 @@ func TestCompileValidatingPolicyExpression(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCompile(b *testing.B) {
|
||||
compiler := NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()))
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
options := OptionalVariableDeclarations{HasParams: rand.Int()%2 == 0, HasAuthorizer: rand.Int()%2 == 0}
|
||||
|
||||
result := compiler.CompileCELExpression(&fakeValidationCondition{
|
||||
Expression: "object.foo < object.bar",
|
||||
}, options, environment.StoredExpressions)
|
||||
if result.Error != nil {
|
||||
b.Fatal(result.Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type fakeValidationCondition struct {
|
||||
Expression string
|
||||
}
|
||||
|
||||
@@ -32,15 +32,17 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/cel"
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
"k8s.io/apiserver/pkg/cel/library"
|
||||
)
|
||||
|
||||
// filterCompiler implement the interface FilterCompiler.
|
||||
type filterCompiler struct {
|
||||
compiler Compiler
|
||||
}
|
||||
|
||||
func NewFilterCompiler() FilterCompiler {
|
||||
return &filterCompiler{}
|
||||
func NewFilterCompiler(env *environment.EnvSet) FilterCompiler {
|
||||
return &filterCompiler{compiler: NewCompiler(env)}
|
||||
}
|
||||
|
||||
type evaluationActivation struct {
|
||||
@@ -75,13 +77,13 @@ func (a *evaluationActivation) Parent() interpreter.Activation {
|
||||
}
|
||||
|
||||
// Compile compiles the cel expressions defined in the ExpressionAccessors into a Filter
|
||||
func (c *filterCompiler) Compile(expressionAccessors []ExpressionAccessor, options OptionalVariableDeclarations, perCallLimit uint64) Filter {
|
||||
func (c *filterCompiler) Compile(expressionAccessors []ExpressionAccessor, options OptionalVariableDeclarations, mode environment.Type) Filter {
|
||||
compilationResults := make([]CompilationResult, len(expressionAccessors))
|
||||
for i, expressionAccessor := range expressionAccessors {
|
||||
if expressionAccessor == nil {
|
||||
continue
|
||||
}
|
||||
compilationResults[i] = CompileCELExpression(expressionAccessor, options, perCallLimit)
|
||||
compilationResults[i] = c.compiler.CompileCELExpression(expressionAccessor, options, mode)
|
||||
}
|
||||
return NewFilter(compilationResults)
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ import (
|
||||
celtypes "github.com/google/cel-go/common/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"k8s.io/utils/pointer"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
@@ -38,7 +40,7 @@ import (
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
apiservercel "k8s.io/apiserver/pkg/cel"
|
||||
"k8s.io/utils/pointer"
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
)
|
||||
|
||||
type condition struct {
|
||||
@@ -91,8 +93,8 @@ func TestCompile(t *testing.T) {
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var c filterCompiler
|
||||
e := c.Compile(tc.validation, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false}, celconfig.PerCallLimit)
|
||||
c := filterCompiler{compiler: NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()))}
|
||||
e := c.Compile(tc.validation, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false}, environment.NewExpressions)
|
||||
if e == nil {
|
||||
t.Fatalf("unexpected nil validator")
|
||||
}
|
||||
@@ -643,11 +645,20 @@ func TestFilter(t *testing.T) {
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
c := filterCompiler{}
|
||||
if tc.testPerCallLimit == 0 {
|
||||
tc.testPerCallLimit = celconfig.PerCallLimit
|
||||
}
|
||||
f := c.Compile(tc.validations, OptionalVariableDeclarations{HasParams: tc.hasParamKind, HasAuthorizer: tc.authorizer != nil}, tc.testPerCallLimit)
|
||||
env, err := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()).Extend(
|
||||
environment.VersionedOptions{
|
||||
IntroducedVersion: environment.DefaultCompatibilityVersion(),
|
||||
ProgramOptions: []celgo.ProgramOption{celgo.CostLimit(tc.testPerCallLimit)},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c := NewFilterCompiler(env)
|
||||
f := c.Compile(tc.validations, OptionalVariableDeclarations{HasParams: tc.hasParamKind, HasAuthorizer: tc.authorizer != nil}, environment.NewExpressions)
|
||||
if f == nil {
|
||||
t.Fatalf("unexpected nil validator")
|
||||
}
|
||||
@@ -789,8 +800,8 @@ func TestRuntimeCELCostBudget(t *testing.T) {
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
c := filterCompiler{}
|
||||
f := c.Compile(tc.validations, OptionalVariableDeclarations{HasParams: tc.hasParamKind, HasAuthorizer: false}, celconfig.PerCallLimit)
|
||||
c := filterCompiler{compiler: NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()))}
|
||||
f := c.Compile(tc.validations, OptionalVariableDeclarations{HasParams: tc.hasParamKind, HasAuthorizer: false}, environment.NewExpressions)
|
||||
if f == nil {
|
||||
t.Fatalf("unexpected nil validator")
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
)
|
||||
|
||||
type ExpressionAccessor interface {
|
||||
@@ -57,8 +58,7 @@ type OptionalVariableDeclarations struct {
|
||||
// FilterCompiler contains a function to assist with converting types and values to/from CEL-typed values.
|
||||
type FilterCompiler interface {
|
||||
// Compile is used for the cel expression compilation
|
||||
// perCallLimit was added for testing purpose only. Callers should always use const PerCallLimit from k8s.io/apiserver/pkg/apis/cel/config.go as input.
|
||||
Compile(expressions []ExpressionAccessor, optionalDecls OptionalVariableDeclarations, perCallLimit uint64) Filter
|
||||
Compile(expressions []ExpressionAccessor, optionalDecls OptionalVariableDeclarations, envType environment.Type) Filter
|
||||
}
|
||||
|
||||
// OptionalVariableBindings provides expression bindings for optional CEL variables.
|
||||
|
||||
@@ -48,6 +48,7 @@ import (
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
|
||||
auditinternal "k8s.io/apiserver/pkg/apis/audit"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
"k8s.io/apiserver/pkg/features"
|
||||
"k8s.io/apiserver/pkg/warning"
|
||||
dynamicfake "k8s.io/client-go/dynamic/fake"
|
||||
@@ -210,7 +211,7 @@ func (f *fakeCompiler) HasSynced() bool {
|
||||
func (f *fakeCompiler) Compile(
|
||||
expressions []cel.ExpressionAccessor,
|
||||
options cel.OptionalVariableDeclarations,
|
||||
perCallLimit uint64,
|
||||
envType environment.Type,
|
||||
) cel.Filter {
|
||||
if len(expressions) > 0 && expressions[0] != nil {
|
||||
key := expressions[0].GetExpression()
|
||||
@@ -708,9 +709,9 @@ func must3[T any, I any](val T, _ I, err error) T {
|
||||
return val
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// //////////////////////////////////////////////////////////////////////////////
|
||||
// Functionality Tests
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// //////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
func TestPluginNotReady(t *testing.T) {
|
||||
reset()
|
||||
@@ -1031,7 +1032,7 @@ func TestReconfigureBinding(t *testing.T) {
|
||||
|
||||
// Expect validation to fail the third time due to validation failure
|
||||
require.ErrorContains(t, err, `Denied`, "expected a true policy failure, not a configuration error")
|
||||
//require.Equal(t, []*unstructured.Unstructured{fakeParams, fakeParams2}, passedParams, "expected call to `Validate` to cause call to evaluator")
|
||||
// require.Equal(t, []*unstructured.Unstructured{fakeParams, fakeParams2}, passedParams, "expected call to `Validate` to cause call to evaluator")
|
||||
require.Equal(t, 2, numCompiles, "expect changing binding causes a recompile")
|
||||
}
|
||||
|
||||
@@ -1162,7 +1163,7 @@ func TestRemoveBinding(t *testing.T) {
|
||||
),
|
||||
`Denied`)
|
||||
|
||||
//require.Equal(t, []*unstructured.Unstructured{fakeParams}, passedParams)
|
||||
// require.Equal(t, []*unstructured.Unstructured{fakeParams}, passedParams)
|
||||
require.NoError(t, tracker.Delete(bindingsGVR, denyBinding.Namespace, denyBinding.Name))
|
||||
require.NoError(t, waitForReconcileDeletion(testContext, controller, denyBinding))
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ import (
|
||||
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/matching"
|
||||
celconfig "k8s.io/apiserver/pkg/apis/cel"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
"k8s.io/apiserver/pkg/cel/openapi/resolver"
|
||||
"k8s.io/apiserver/pkg/warning"
|
||||
"k8s.io/client-go/dynamic"
|
||||
@@ -140,7 +141,7 @@ func NewAdmissionController(
|
||||
client,
|
||||
dynamicClient,
|
||||
typeChecker,
|
||||
cel.NewFilterCompiler(),
|
||||
cel.NewFilterCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())),
|
||||
NewMatcher(matching.NewMatcher(informerFactory.Core().V1().Namespaces().Lister(), client)),
|
||||
generic.NewInformer[*v1alpha1.ValidatingAdmissionPolicy](
|
||||
informerFactory.Admissionregistration().V1alpha1().ValidatingAdmissionPolicies().Informer()),
|
||||
|
||||
@@ -36,8 +36,8 @@ import (
|
||||
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
|
||||
celconfig "k8s.io/apiserver/pkg/apis/cel"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/dynamic/dynamicinformer"
|
||||
"k8s.io/client-go/informers"
|
||||
@@ -512,13 +512,13 @@ func (c *policyController) latestPolicyData() []policyData {
|
||||
for i := range matchConditions {
|
||||
matchExpressionAccessors[i] = (*matchconditions.MatchCondition)(&matchConditions[i])
|
||||
}
|
||||
matcher = matchconditions.NewMatcher(c.filterCompiler.Compile(matchExpressionAccessors, optionalVars, celconfig.PerCallLimit), c.authz, failurePolicy, "validatingadmissionpolicy", definitionInfo.lastReconciledValue.Name)
|
||||
matcher = matchconditions.NewMatcher(c.filterCompiler.Compile(matchExpressionAccessors, optionalVars, environment.StoredExpressions), c.authz, failurePolicy, "validatingadmissionpolicy", definitionInfo.lastReconciledValue.Name)
|
||||
}
|
||||
bindingInfo.validator = c.newValidator(
|
||||
c.filterCompiler.Compile(convertv1alpha1Validations(definitionInfo.lastReconciledValue.Spec.Validations), optionalVars, celconfig.PerCallLimit),
|
||||
c.filterCompiler.Compile(convertv1alpha1Validations(definitionInfo.lastReconciledValue.Spec.Validations), optionalVars, environment.StoredExpressions),
|
||||
matcher,
|
||||
c.filterCompiler.Compile(convertv1alpha1AuditAnnotations(definitionInfo.lastReconciledValue.Spec.AuditAnnotations), optionalVars, celconfig.PerCallLimit),
|
||||
c.filterCompiler.Compile(convertV1Alpha1MessageExpressions(definitionInfo.lastReconciledValue.Spec.Validations), expressionOptionalVars, celconfig.PerCallLimit),
|
||||
c.filterCompiler.Compile(convertv1alpha1AuditAnnotations(definitionInfo.lastReconciledValue.Spec.AuditAnnotations), optionalVars, environment.StoredExpressions),
|
||||
c.filterCompiler.Compile(convertV1Alpha1MessageExpressions(definitionInfo.lastReconciledValue.Spec.Validations), expressionOptionalVars, environment.StoredExpressions),
|
||||
failurePolicy,
|
||||
c.authz,
|
||||
)
|
||||
|
||||
@@ -21,20 +21,20 @@ import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/cel-go/cel"
|
||||
"github.com/google/cel-go/common/types/ref"
|
||||
|
||||
"k8s.io/api/admissionregistration/v1alpha1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/apimachinery/pkg/util/version"
|
||||
plugincel "k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||
apiservercel "k8s.io/apiserver/pkg/cel"
|
||||
"k8s.io/apiserver/pkg/cel/common"
|
||||
"k8s.io/apiserver/pkg/cel/library"
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
"k8s.io/apiserver/pkg/cel/openapi"
|
||||
"k8s.io/apiserver/pkg/cel/openapi/resolver"
|
||||
"k8s.io/klog/v2"
|
||||
@@ -128,7 +128,7 @@ func (c *TypeChecker) CheckExpressions(expressions []string, hasParams bool, pol
|
||||
for i, gvk := range gvks {
|
||||
s := schemas[i]
|
||||
issues, err := c.checkExpression(exp, hasParams, typeOverwrite{
|
||||
object: common.SchemaDeclType(s, true),
|
||||
object: common.SchemaDeclType(s, true).MaybeAssignTypeName(generateUniqueTypeName(gvk.Kind)),
|
||||
params: paramsDeclType,
|
||||
})
|
||||
// save even if no issues are found, for the sake of formatting.
|
||||
@@ -144,6 +144,10 @@ func (c *TypeChecker) CheckExpressions(expressions []string, hasParams bool, pol
|
||||
return allWarnings
|
||||
}
|
||||
|
||||
func generateUniqueTypeName(kind string) string {
|
||||
return fmt.Sprintf("%s%d", kind, time.Now().Nanosecond())
|
||||
}
|
||||
|
||||
// formatWarning converts the resulting issues and possible error during
|
||||
// type checking into a human-readable string
|
||||
func (c *TypeChecker) formatWarning(results []typeCheckingResult) string {
|
||||
@@ -169,7 +173,7 @@ func (c *TypeChecker) declType(gvk schema.GroupVersionKind) (*apiservercel.DeclT
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return common.SchemaDeclType(&openapi.Schema{Schema: s}, true), nil
|
||||
return common.SchemaDeclType(&openapi.Schema{Schema: s}, true).MaybeAssignTypeName(generateUniqueTypeName(gvk.Kind)), nil
|
||||
}
|
||||
|
||||
func (c *TypeChecker) paramsType(policy *v1alpha1.ValidatingAdmissionPolicy) schema.GroupVersionKind {
|
||||
@@ -314,122 +318,51 @@ func sortGVKList(list []schema.GroupVersionKind) []schema.GroupVersionKind {
|
||||
}
|
||||
|
||||
func buildEnv(hasParams bool, types typeOverwrite) (*cel.Env, error) {
|
||||
baseEnv, err := getBaseEnv()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reg := apiservercel.NewRegistry(baseEnv)
|
||||
baseEnv := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())
|
||||
requestType := plugincel.BuildRequestType()
|
||||
|
||||
var varOpts []cel.EnvOption
|
||||
var rts []*apiservercel.RuleTypes
|
||||
var declTypes []*apiservercel.DeclType
|
||||
|
||||
// request, hand-crafted type
|
||||
rt, opts, err := createRuleTypesAndOptions(reg, requestType, plugincel.RequestVarName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rts = append(rts, rt)
|
||||
varOpts = append(varOpts, opts...)
|
||||
declTypes = append(declTypes, requestType)
|
||||
varOpts = append(varOpts, createVariableOpts(requestType, plugincel.RequestVarName)...)
|
||||
|
||||
// object and oldObject, same type, type(s) resolved from constraints
|
||||
rt, opts, err = createRuleTypesAndOptions(reg, types.object, plugincel.ObjectVarName, plugincel.OldObjectVarName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rts = append(rts, rt)
|
||||
varOpts = append(varOpts, opts...)
|
||||
declTypes = append(declTypes, types.object)
|
||||
varOpts = append(varOpts, createVariableOpts(types.object, plugincel.ObjectVarName, plugincel.OldObjectVarName)...)
|
||||
|
||||
// params, defined by ParamKind
|
||||
if hasParams {
|
||||
rt, opts, err := createRuleTypesAndOptions(reg, types.params, plugincel.ParamsVarName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rts = append(rts, rt)
|
||||
varOpts = append(varOpts, opts...)
|
||||
if hasParams && types.params != nil {
|
||||
declTypes = append(declTypes, types.params)
|
||||
varOpts = append(varOpts, createVariableOpts(types.params, plugincel.ParamsVarName)...)
|
||||
}
|
||||
|
||||
opts, err = ruleTypesOpts(rts, baseEnv.TypeProvider())
|
||||
env, err := baseEnv.Extend(
|
||||
environment.VersionedOptions{
|
||||
// Feature epoch was actually 1.26, but we artificially set it to 1.0 because these
|
||||
// options should always be present.
|
||||
IntroducedVersion: version.MajorMinor(1, 0),
|
||||
EnvOptions: varOpts,
|
||||
DeclTypes: declTypes,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opts = append(opts, varOpts...) // add variables after ruleTypes.
|
||||
env, err := baseEnv.Extend(opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return env, nil
|
||||
return env.Env(environment.StoredExpressions)
|
||||
}
|
||||
|
||||
// createRuleTypeAndOptions creates the cel RuleTypes and a slice of EnvOption
|
||||
// createVariableOpts creates a slice of EnvOption
|
||||
// that can be used for creating a CEL env containing variables of declType.
|
||||
// declType can be nil, in which case the variables will be of DynType.
|
||||
func createRuleTypesAndOptions(registry *apiservercel.Registry, declType *apiservercel.DeclType, variables ...string) (*apiservercel.RuleTypes, []cel.EnvOption, error) {
|
||||
func createVariableOpts(declType *apiservercel.DeclType, variables ...string) []cel.EnvOption {
|
||||
opts := make([]cel.EnvOption, 0, len(variables))
|
||||
// untyped, use DynType
|
||||
if declType == nil {
|
||||
for _, v := range variables {
|
||||
opts = append(opts, cel.Variable(v, cel.DynType))
|
||||
}
|
||||
return nil, opts, nil
|
||||
}
|
||||
// create a RuleType for the given type
|
||||
rt, err := apiservercel.NewRuleTypes(declType.TypeName(), declType, registry)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if rt == nil {
|
||||
return nil, nil, nil
|
||||
t := cel.DynType
|
||||
if declType != nil {
|
||||
t = declType.CelType()
|
||||
}
|
||||
for _, v := range variables {
|
||||
opts = append(opts, cel.Variable(v, declType.CelType()))
|
||||
opts = append(opts, cel.Variable(v, t))
|
||||
}
|
||||
return rt, opts, nil
|
||||
return opts
|
||||
}
|
||||
|
||||
func ruleTypesOpts(ruleTypes []*apiservercel.RuleTypes, underlyingTypeProvider ref.TypeProvider) ([]cel.EnvOption, error) {
|
||||
var providers []ref.TypeProvider // may be unused, too small to matter
|
||||
var adapters []ref.TypeAdapter
|
||||
for _, rt := range ruleTypes {
|
||||
if rt != nil {
|
||||
withTP, err := rt.WithTypeProvider(underlyingTypeProvider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
providers = append(providers, withTP)
|
||||
adapters = append(adapters, withTP)
|
||||
}
|
||||
}
|
||||
var tp ref.TypeProvider
|
||||
var ta ref.TypeAdapter
|
||||
switch len(providers) {
|
||||
case 0:
|
||||
return nil, nil
|
||||
case 1:
|
||||
tp = providers[0]
|
||||
ta = adapters[0]
|
||||
default:
|
||||
tp = &apiservercel.CompositedTypeProvider{Providers: providers}
|
||||
ta = &apiservercel.CompositedTypeAdapter{Adapters: adapters}
|
||||
}
|
||||
return []cel.EnvOption{cel.CustomTypeProvider(tp), cel.CustomTypeAdapter(ta)}, nil
|
||||
}
|
||||
|
||||
func getBaseEnv() (*cel.Env, error) {
|
||||
typeCheckingBaseEnvInit.Do(func() {
|
||||
var opts []cel.EnvOption
|
||||
opts = append(opts, cel.HomogeneousAggregateLiterals())
|
||||
// Validate function declarations once during base env initialization,
|
||||
// so they don't need to be evaluated each time a CEL rule is compiled.
|
||||
// This is a relatively expensive operation.
|
||||
opts = append(opts, cel.EagerlyValidateDeclarations(true), cel.DefaultUTCTimeZone(true))
|
||||
opts = append(opts, library.ExtensionLibs...)
|
||||
typeCheckingBaseEnv, typeCheckingBaseEnvError = cel.NewEnv(opts...)
|
||||
})
|
||||
return typeCheckingBaseEnv, typeCheckingBaseEnvError
|
||||
}
|
||||
|
||||
var typeCheckingBaseEnv *cel.Env
|
||||
var typeCheckingBaseEnvError error
|
||||
var typeCheckingBaseEnvInit sync.Once
|
||||
|
||||
@@ -36,6 +36,7 @@ import (
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
|
||||
celconfig "k8s.io/apiserver/pkg/apis/cel"
|
||||
apiservercel "k8s.io/apiserver/pkg/cel"
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
)
|
||||
|
||||
var _ cel.Filter = &fakeCelFilter{}
|
||||
@@ -928,8 +929,8 @@ func TestContextCanceled(t *testing.T) {
|
||||
|
||||
fakeAttr := admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{}, "default", "foo", schema.GroupVersionResource{}, "", admission.Create, nil, false, nil)
|
||||
fakeVersionedAttr, _ := admission.NewVersionedAttributes(fakeAttr, schema.GroupVersionKind{}, nil)
|
||||
fc := cel.NewFilterCompiler()
|
||||
f := fc.Compile([]cel.ExpressionAccessor{&ValidationCondition{Expression: "[1,2,3,4,5,6,7,8,9,10].map(x, [1,2,3,4,5,6,7,8,9,10].map(y, x*y)) == []"}}, cel.OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false}, celconfig.PerCallLimit)
|
||||
fc := cel.NewFilterCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()))
|
||||
f := fc.Compile([]cel.ExpressionAccessor{&ValidationCondition{Expression: "[1,2,3,4,5,6,7,8,9,10].map(x, [1,2,3,4,5,6,7,8,9,10].map(y, x*y)) == []"}}, cel.OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false}, environment.StoredExpressions)
|
||||
v := validator{
|
||||
failPolicy: &fail,
|
||||
celMatcher: &fakeCELMatcher{matches: true},
|
||||
|
||||
@@ -26,8 +26,8 @@ import (
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/namespace"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/object"
|
||||
celconfig "k8s.io/apiserver/pkg/apis/cel"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
webhookutil "k8s.io/apiserver/pkg/util/webhook"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
@@ -140,7 +140,7 @@ func (m *mutatingWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompiler
|
||||
HasParams: false,
|
||||
HasAuthorizer: true,
|
||||
},
|
||||
celconfig.PerCallLimit,
|
||||
environment.StoredExpressions,
|
||||
), authorizer, m.FailurePolicy, "validating", m.Name)
|
||||
})
|
||||
return m.compiledMatcher
|
||||
@@ -268,7 +268,7 @@ func (v *validatingWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompil
|
||||
HasParams: false,
|
||||
HasAuthorizer: true,
|
||||
},
|
||||
celconfig.PerCallLimit,
|
||||
environment.StoredExpressions,
|
||||
), authorizer, v.FailurePolicy, "validating", v.Name)
|
||||
})
|
||||
return v.compiledMatcher
|
||||
|
||||
@@ -35,6 +35,7 @@ import (
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/object"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/rules"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
webhookutil "k8s.io/apiserver/pkg/util/webhook"
|
||||
"k8s.io/client-go/informers"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
@@ -97,7 +98,7 @@ func NewWebhook(handler *admission.Handler, configFile io.Reader, sourceFactory
|
||||
namespaceMatcher: &namespace.Matcher{},
|
||||
objectMatcher: &object.Matcher{},
|
||||
dispatcher: dispatcherFactory(&cm),
|
||||
filterCompiler: cel.NewFilterCompiler(),
|
||||
filterCompiler: cel.NewFilterCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
/*
|
||||
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 cel
|
||||
|
||||
import (
|
||||
"github.com/google/cel-go/common/types/ref"
|
||||
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
|
||||
)
|
||||
|
||||
var _ ref.TypeProvider = (*CompositedTypeProvider)(nil)
|
||||
var _ ref.TypeAdapter = (*CompositedTypeAdapter)(nil)
|
||||
|
||||
// CompositedTypeProvider is the provider that tries each of the underlying
|
||||
// providers in order, and returns result of the first successful attempt.
|
||||
type CompositedTypeProvider struct {
|
||||
// Providers contains the underlying type providers.
|
||||
// If Providers is empty, the CompositedTypeProvider becomes no-op provider.
|
||||
Providers []ref.TypeProvider
|
||||
}
|
||||
|
||||
// EnumValue finds out the numeric value of the given enum name.
|
||||
// The result comes from first provider that returns non-nil.
|
||||
func (c *CompositedTypeProvider) EnumValue(enumName string) ref.Val {
|
||||
for _, p := range c.Providers {
|
||||
val := p.EnumValue(enumName)
|
||||
if val != nil {
|
||||
return val
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FindIdent takes a qualified identifier name and returns a Value if one
|
||||
// exists. The result comes from first provider that returns non-nil.
|
||||
func (c *CompositedTypeProvider) FindIdent(identName string) (ref.Val, bool) {
|
||||
for _, p := range c.Providers {
|
||||
val, ok := p.FindIdent(identName)
|
||||
if ok {
|
||||
return val, ok
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// FindType finds the Type given a qualified type name, or return false
|
||||
// if none of the providers finds the type.
|
||||
// If any of the providers find the type, the first provider that returns true
|
||||
// will be the result.
|
||||
func (c *CompositedTypeProvider) FindType(typeName string) (*exprpb.Type, bool) {
|
||||
for _, p := range c.Providers {
|
||||
typ, ok := p.FindType(typeName)
|
||||
if ok {
|
||||
return typ, ok
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// FindFieldType returns the field type for a checked type value. Returns
|
||||
// false if none of the providers can find the type.
|
||||
// If multiple providers can find the field, the result is taken from
|
||||
// the first that does.
|
||||
func (c *CompositedTypeProvider) FindFieldType(messageType string, fieldName string) (*ref.FieldType, bool) {
|
||||
for _, p := range c.Providers {
|
||||
ft, ok := p.FindFieldType(messageType, fieldName)
|
||||
if ok {
|
||||
return ft, ok
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// NewValue creates a new type value from a qualified name and map of field
|
||||
// name to value.
|
||||
// If multiple providers can create the new type, the first that returns
|
||||
// non-nil will decide the result.
|
||||
func (c *CompositedTypeProvider) NewValue(typeName string, fields map[string]ref.Val) ref.Val {
|
||||
for _, p := range c.Providers {
|
||||
v := p.NewValue(typeName, fields)
|
||||
if v != nil {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CompositedTypeAdapter is the adapter that tries each of the underlying
|
||||
// type adapter in order until the first successfully conversion.
|
||||
type CompositedTypeAdapter struct {
|
||||
// Adapters contains underlying type adapters.
|
||||
// If Adapters is empty, the CompositedTypeAdapter becomes a no-op adapter.
|
||||
Adapters []ref.TypeAdapter
|
||||
}
|
||||
|
||||
// NativeToValue takes the value and convert it into a ref.Val
|
||||
// The result comes from the first TypeAdapter that returns non-nil.
|
||||
func (c *CompositedTypeAdapter) NativeToValue(value interface{}) ref.Val {
|
||||
for _, a := range c.Adapters {
|
||||
v := a.NativeToValue(value)
|
||||
if v != nil {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
117
staging/src/k8s.io/apiserver/pkg/cel/environment/base.go
Normal file
117
staging/src/k8s.io/apiserver/pkg/cel/environment/base.go
Normal file
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
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 environment
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/google/cel-go/cel"
|
||||
"github.com/google/cel-go/ext"
|
||||
"golang.org/x/sync/singleflight"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/version"
|
||||
celconfig "k8s.io/apiserver/pkg/apis/cel"
|
||||
"k8s.io/apiserver/pkg/cel/library"
|
||||
)
|
||||
|
||||
// DefaultCompatibilityVersion returns a default compatibility version for use with EnvSet
|
||||
// that guarantees compatibility with CEL features/libraries/parameters understood by
|
||||
// an n-1 version
|
||||
//
|
||||
// This default will be set to no more than n-1 the current Kubernetes major.minor version.
|
||||
//
|
||||
// Note that a default version number less than n-1 indicates a wider range of version
|
||||
// compatibility than strictly required for rollback. A wide range of compatibility is
|
||||
// desirable because it means that CEL expressions are portable across a wider range
|
||||
// of Kubernetes versions.
|
||||
func DefaultCompatibilityVersion() *version.Version {
|
||||
return version.MajorMinor(1, 27)
|
||||
}
|
||||
|
||||
var baseOpts = []VersionedOptions{
|
||||
{
|
||||
// CEL 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.HomogeneousAggregateLiterals(),
|
||||
// Validate function declarations once during base env initialization,
|
||||
// so they don't need to be evaluated each time a CEL rule is compiled.
|
||||
// This is a relatively expensive operation.
|
||||
cel.EagerlyValidateDeclarations(true),
|
||||
cel.DefaultUTCTimeZone(true),
|
||||
|
||||
ext.Strings(),
|
||||
library.URLs(),
|
||||
library.Regex(),
|
||||
library.Lists(),
|
||||
},
|
||||
ProgramOptions: []cel.ProgramOption{
|
||||
cel.EvalOptions(cel.OptOptimize, cel.OptTrackCost),
|
||||
cel.CostLimit(celconfig.PerCallLimit),
|
||||
},
|
||||
},
|
||||
{
|
||||
IntroducedVersion: version.MajorMinor(1, 27),
|
||||
EnvOptions: []cel.EnvOption{
|
||||
library.Authz(),
|
||||
},
|
||||
},
|
||||
{
|
||||
IntroducedVersion: version.MajorMinor(1, 28),
|
||||
EnvOptions: []cel.EnvOption{
|
||||
cel.CrossTypeNumericComparisons(true),
|
||||
// TODO: Add CEL Optionals once we bump cel-go
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// MustBaseEnvSet returns the common CEL base environments for Kubernetes for Version, or panics
|
||||
// if the version is nil, or does not have major and minor components.
|
||||
//
|
||||
// The returned environment contains function libraries, language settings, optimizations and
|
||||
// runtime cost limits appropriate CEL as it is used in Kubernetes.
|
||||
//
|
||||
// The returned environment contains no CEL variable definitions or custom type declarations and
|
||||
// should be extended to construct environments with the appropriate variable definitions,
|
||||
// type declarations and any other needed configuration.
|
||||
func MustBaseEnvSet(ver *version.Version) *EnvSet {
|
||||
if ver == nil {
|
||||
panic("version must be non-nil")
|
||||
}
|
||||
if len(ver.Components()) < 2 {
|
||||
panic(fmt.Sprintf("version must contain an major and minor component, but got: %s", ver.String()))
|
||||
}
|
||||
key := strconv.FormatUint(uint64(ver.Major()), 10) + "." + strconv.FormatUint(uint64(ver.Minor()), 10)
|
||||
if entry, ok := baseEnvs.Load(key); ok {
|
||||
return entry.(*EnvSet)
|
||||
}
|
||||
|
||||
entry, _, _ := baseEnvsSingleflight.Do(key, func() (interface{}, error) {
|
||||
entry := mustNewEnvSet(ver, baseOpts)
|
||||
baseEnvs.Store(key, entry)
|
||||
return entry, nil
|
||||
})
|
||||
return entry.(*EnvSet)
|
||||
}
|
||||
|
||||
var (
|
||||
baseEnvs = sync.Map{}
|
||||
baseEnvsSingleflight = &singleflight.Group{}
|
||||
)
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
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 environment
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/version"
|
||||
)
|
||||
|
||||
// BenchmarkLoadBaseEnv is expected to be very fast, because a
|
||||
// a cached environment is loaded for each MustBaseEnvSet call.
|
||||
func BenchmarkLoadBaseEnv(b *testing.B) {
|
||||
ver := DefaultCompatibilityVersion()
|
||||
MustBaseEnvSet(ver)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
MustBaseEnvSet(ver)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkLoadBaseEnvDifferentVersions is expected to be relatively slow, because a
|
||||
// a new environment must be created for each MustBaseEnvSet call.
|
||||
func BenchmarkLoadBaseEnvDifferentVersions(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
MustBaseEnvSet(version.MajorMinor(1, uint(i)))
|
||||
}
|
||||
}
|
||||
274
staging/src/k8s.io/apiserver/pkg/cel/environment/environment.go
Normal file
274
staging/src/k8s.io/apiserver/pkg/cel/environment/environment.go
Normal file
@@ -0,0 +1,274 @@
|
||||
/*
|
||||
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 environment
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"github.com/google/cel-go/cel"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/version"
|
||||
apiservercel "k8s.io/apiserver/pkg/cel"
|
||||
)
|
||||
|
||||
// Type defines the different types of CEL environments used in Kubernetes.
|
||||
// CEL environments are used to compile and evaluate CEL expressions.
|
||||
// Environments include:
|
||||
// - Function libraries
|
||||
// - Variables
|
||||
// - Types (both core CEL types and Kubernetes types)
|
||||
// - Other CEL environment and program options
|
||||
type Type string
|
||||
|
||||
const (
|
||||
// NewExpressions is used to validate new or modified expressions in
|
||||
// requests that write expressions to API resources.
|
||||
//
|
||||
// This environment type is compatible with a specific Kubernetes
|
||||
// major/minor version. To ensure safe rollback, this environment type
|
||||
// may not include all the function libraries, variables, type declarations, and CEL
|
||||
// language settings available in the StoredExpressions environment type.
|
||||
//
|
||||
// NewExpressions must be used to validate (parse, compile, type check)
|
||||
// all new or modified CEL expressions before they are written to storage.
|
||||
NewExpressions Type = "NewExpressions"
|
||||
|
||||
// StoredExpressions is used to compile and run CEL expressions that have been
|
||||
// persisted to storage.
|
||||
//
|
||||
// This environment type is compatible with CEL expressions that have been
|
||||
// persisted to storage by all known versions of Kubernetes. This is the most
|
||||
// permissive environment available.
|
||||
//
|
||||
// StoredExpressions is appropriate for use with CEL expressions in
|
||||
// configuration files.
|
||||
StoredExpressions Type = "StoredExpressions"
|
||||
)
|
||||
|
||||
// EnvSet manages the creation and extension of CEL environments. Each EnvSet contains
|
||||
// both an NewExpressions and StoredExpressions environment. EnvSets are created
|
||||
// and extended using VersionedOptions so that the EnvSet can prepare environments according
|
||||
// to what options were introduced at which versions.
|
||||
//
|
||||
// Each EnvSet is given a compatibility version when it is created, and prepares the
|
||||
// NewExpressions environment to be compatible with that version. The EnvSet also
|
||||
// prepares StoredExpressions to be compatible with all known versions of Kubernetes.
|
||||
type EnvSet struct {
|
||||
// compatibilityVersion is the version that all configuration in
|
||||
// the NewExpressions environment is compatible with.
|
||||
compatibilityVersion *version.Version
|
||||
|
||||
// newExpressions is an environment containing only configuration
|
||||
// in this EnvSet that is enabled at this compatibilityVersion.
|
||||
newExpressions *cel.Env
|
||||
|
||||
// storedExpressions is an environment containing the latest configuration
|
||||
// in this EnvSet.
|
||||
storedExpressions *cel.Env
|
||||
}
|
||||
|
||||
func newEnvSet(compatibilityVersion *version.Version, opts []VersionedOptions) (*EnvSet, error) {
|
||||
base, err := cel.NewEnv()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
baseSet := EnvSet{compatibilityVersion: compatibilityVersion, newExpressions: base, storedExpressions: base}
|
||||
return baseSet.Extend(opts...)
|
||||
}
|
||||
|
||||
func mustNewEnvSet(ver *version.Version, opts []VersionedOptions) *EnvSet {
|
||||
envSet, err := newEnvSet(ver, opts)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Default environment misconfigured: %v", err))
|
||||
}
|
||||
return envSet
|
||||
}
|
||||
|
||||
// NewExpressionsEnv returns the NewExpressions environment Type for this EnvSet.
|
||||
// See NewExpressions for details.
|
||||
func (e *EnvSet) NewExpressionsEnv() *cel.Env {
|
||||
return e.newExpressions
|
||||
}
|
||||
|
||||
// StoredExpressionsEnv returns the StoredExpressions environment Type for this EnvSet.
|
||||
// See StoredExpressions for details.
|
||||
func (e *EnvSet) StoredExpressionsEnv() *cel.Env {
|
||||
return e.storedExpressions
|
||||
}
|
||||
|
||||
// Env returns the CEL environment for the given Type.
|
||||
func (e *EnvSet) Env(envType Type) (*cel.Env, error) {
|
||||
switch envType {
|
||||
case NewExpressions:
|
||||
return e.newExpressions, nil
|
||||
case StoredExpressions:
|
||||
return e.storedExpressions, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported environment type: %v", envType)
|
||||
}
|
||||
}
|
||||
|
||||
// VersionedOptions provides a set of CEL configuration options as well as the version the
|
||||
// options were introduced and, optionally, the version the options were removed.
|
||||
type VersionedOptions struct {
|
||||
// IntroducedVersion is the version at which these options were introduced.
|
||||
// The NewExpressions environment will only include options introduced at or before the
|
||||
// compatibility version of the EnvSet.
|
||||
//
|
||||
// For example, to configure a CEL environment with an "object" variable bound to a
|
||||
// resource kind, first create a DeclType from the groupVersionKind of the resource and then
|
||||
// populate a VersionedOptions with the variable and the type:
|
||||
//
|
||||
// schema := schemaResolver.ResolveSchema(groupVersionKind)
|
||||
// objectType := apiservercel.SchemaDeclType(schema, true)
|
||||
// ...
|
||||
// VersionOptions{
|
||||
// IntroducedVersion: version.MajorMinor(1, 26),
|
||||
// DeclTypes: []*apiservercel.DeclType{ objectType },
|
||||
// EnvOptions: []cel.EnvOption{ cel.Variable("object", objectType.CelType()) },
|
||||
// },
|
||||
//
|
||||
// To create an DeclType from a CRD, use a structural schema. For example:
|
||||
//
|
||||
// schema := structuralschema.NewStructural(crdJSONProps)
|
||||
// objectType := apiservercel.SchemaDeclType(schema, true)
|
||||
//
|
||||
// Required.
|
||||
IntroducedVersion *version.Version
|
||||
// RemovedVersion is the version at which these options were removed.
|
||||
// The NewExpressions environment will not include options removed at or before the
|
||||
// compatibility version of the EnvSet.
|
||||
//
|
||||
// All option removals must be backward compatible; the removal must either be paired
|
||||
// with a compatible replacement introduced at the same version, or the removal must be non-breaking.
|
||||
// The StoredExpressions environment will not include removed options.
|
||||
//
|
||||
// A function library may be upgraded by setting the RemovedVersion of the old library
|
||||
// to the same value as the IntroducedVersion of the new library. The new library must
|
||||
// be backward compatible with the old library.
|
||||
//
|
||||
// For example:
|
||||
//
|
||||
// VersionOptions{
|
||||
// IntroducedVersion: version.MajorMinor(1, 26), RemovedVersion: version.MajorMinor(1, 27),
|
||||
// EnvOptions: []cel.EnvOption{ libraries.Example(libraries.ExampleVersion(1)) },
|
||||
// },
|
||||
// VersionOptions{
|
||||
// IntroducedVersion: version.MajorMinor(1, 27),
|
||||
// EnvOptions: []EnvOptions{ libraries.Example(libraries.ExampleVersion(2)) },
|
||||
// },
|
||||
//
|
||||
// Optional.
|
||||
RemovedVersion *version.Version
|
||||
|
||||
// EnvOptions provides CEL EnvOptions. This may be used to add a cel.Variable, a
|
||||
// cel.Library, or to enable other CEL EnvOptions such as language settings.
|
||||
//
|
||||
// If an added cel.Variable has an OpenAPI type, the type must be included in DeclTypes.
|
||||
EnvOptions []cel.EnvOption
|
||||
// ProgramOptions provides CEL ProgramOptions. This may be used to set a cel.CostLimit,
|
||||
// enable optimizations, and set other program level options that should be enabled
|
||||
// for all programs using this environment.
|
||||
ProgramOptions []cel.ProgramOption
|
||||
// DeclTypes provides OpenAPI type declarations to register with the environment.
|
||||
//
|
||||
// If cel.Variables added to EnvOptions refer to a OpenAPI type, the type must be included in
|
||||
// DeclTypes.
|
||||
DeclTypes []*apiservercel.DeclType
|
||||
}
|
||||
|
||||
// Extend returns an EnvSet based on this EnvSet but extended with given VersionedOptions.
|
||||
// This EnvSet is not mutated.
|
||||
// The returned EnvSet has the same compatibility version as the EnvSet that was extended.
|
||||
//
|
||||
// Extend is an expensive operation and each call to Extend that adds DeclTypes increases
|
||||
// the depth of a chain of resolvers. For these reasons, calls to Extend should be kept
|
||||
// to a minimum.
|
||||
//
|
||||
// Some best practices:
|
||||
//
|
||||
// - Minimize calls Extend when handling API requests. Where possible, call Extend
|
||||
// when initializing components.
|
||||
// - If an EnvSets returned by Extend can be used to compile multiple CEL programs,
|
||||
// call Extend once and reuse the returned EnvSets.
|
||||
// - Prefer a single call to Extend with a full list of VersionedOptions over
|
||||
// making multiple calls to Extend.
|
||||
func (e *EnvSet) Extend(options ...VersionedOptions) (*EnvSet, error) {
|
||||
if len(options) > 0 {
|
||||
newExprOpts, err := e.filterAndBuildOpts(e.newExpressions, e.compatibilityVersion, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p, err := e.newExpressions.Extend(newExprOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
storedExprOpt, err := e.filterAndBuildOpts(e.storedExpressions, version.MajorMinor(math.MaxUint, math.MaxUint), options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s, err := e.storedExpressions.Extend(storedExprOpt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &EnvSet{compatibilityVersion: e.compatibilityVersion, newExpressions: p, storedExpressions: s}, nil
|
||||
}
|
||||
return e, nil
|
||||
}
|
||||
|
||||
func (e *EnvSet) filterAndBuildOpts(base *cel.Env, compatVer *version.Version, opts []VersionedOptions) (cel.EnvOption, error) {
|
||||
var envOpts []cel.EnvOption
|
||||
var progOpts []cel.ProgramOption
|
||||
var declTypes []*apiservercel.DeclType
|
||||
|
||||
for _, opt := range opts {
|
||||
if compatVer.AtLeast(opt.IntroducedVersion) && (opt.RemovedVersion == nil || compatVer.LessThan(opt.RemovedVersion)) {
|
||||
envOpts = append(envOpts, opt.EnvOptions...)
|
||||
progOpts = append(progOpts, opt.ProgramOptions...)
|
||||
declTypes = append(declTypes, opt.DeclTypes...)
|
||||
}
|
||||
}
|
||||
|
||||
if len(declTypes) > 0 {
|
||||
provider := apiservercel.NewDeclTypeProvider(declTypes...)
|
||||
providerOpts, err := provider.EnvOptions(base.TypeProvider())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
envOpts = append(envOpts, providerOpts...)
|
||||
}
|
||||
|
||||
combined := cel.Lib(&envLoader{
|
||||
envOpts: envOpts,
|
||||
progOpts: progOpts,
|
||||
})
|
||||
return combined, nil
|
||||
}
|
||||
|
||||
type envLoader struct {
|
||||
envOpts []cel.EnvOption
|
||||
progOpts []cel.ProgramOption
|
||||
}
|
||||
|
||||
func (e *envLoader) CompileOptions() []cel.EnvOption {
|
||||
return e.envOpts
|
||||
}
|
||||
|
||||
func (e *envLoader) ProgramOptions() []cel.ProgramOption {
|
||||
return e.progOpts
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
/*
|
||||
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 environment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/google/cel-go/cel"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/version"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
apiservercel "k8s.io/apiserver/pkg/cel"
|
||||
"k8s.io/apiserver/pkg/cel/library"
|
||||
)
|
||||
|
||||
type envTypeAndVersion struct {
|
||||
version *version.Version
|
||||
envType Type
|
||||
}
|
||||
|
||||
func TestBaseEnvironment(t *testing.T) {
|
||||
widgetsType := apiservercel.NewObjectType("Widget",
|
||||
map[string]*apiservercel.DeclField{
|
||||
"x": {
|
||||
Name: "x",
|
||||
Type: apiservercel.StringType,
|
||||
},
|
||||
})
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
typeVersionCombinations []envTypeAndVersion
|
||||
validExpressions []string
|
||||
invalidExpressions []string
|
||||
activation any
|
||||
opts []VersionedOptions
|
||||
}{
|
||||
{
|
||||
name: "core settings enabled",
|
||||
typeVersionCombinations: []envTypeAndVersion{
|
||||
{version.MajorMinor(1, 23), NewExpressions},
|
||||
{version.MajorMinor(1, 23), StoredExpressions},
|
||||
},
|
||||
validExpressions: []string{
|
||||
"[1, 2, 3].indexOf(2) == 1", // lists
|
||||
"'abc'.contains('bc')", //strings
|
||||
"isURL('http://example.com')", // urls
|
||||
"'a 1 b 2'.find('[0-9]') == '1'", // regex
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "authz disabled",
|
||||
typeVersionCombinations: []envTypeAndVersion{
|
||||
{version.MajorMinor(1, 26), NewExpressions},
|
||||
// always enabled for StoredExpressions
|
||||
},
|
||||
invalidExpressions: []string{"authorizer.path('/healthz').check('get').allowed()"},
|
||||
activation: map[string]any{"authorizer": library.NewAuthorizerVal(nil, fakeAuthorizer{decision: authorizer.DecisionAllow})},
|
||||
opts: []VersionedOptions{
|
||||
{IntroducedVersion: version.MajorMinor(1, 27), EnvOptions: []cel.EnvOption{cel.Variable("authorizer", library.AuthorizerType)}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "authz enabled",
|
||||
typeVersionCombinations: []envTypeAndVersion{
|
||||
{version.MajorMinor(1, 27), NewExpressions},
|
||||
{version.MajorMinor(1, 26), StoredExpressions},
|
||||
},
|
||||
validExpressions: []string{"authorizer.path('/healthz').check('get').allowed()"},
|
||||
activation: map[string]any{"authorizer": library.NewAuthorizerVal(nil, fakeAuthorizer{decision: authorizer.DecisionAllow})},
|
||||
opts: []VersionedOptions{
|
||||
{IntroducedVersion: version.MajorMinor(1, 27), EnvOptions: []cel.EnvOption{cel.Variable("authorizer", library.AuthorizerType)}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cross numeric comparisons disabled",
|
||||
typeVersionCombinations: []envTypeAndVersion{
|
||||
{version.MajorMinor(1, 27), NewExpressions},
|
||||
// always enabled for StoredExpressions
|
||||
},
|
||||
invalidExpressions: []string{"1.5 > 1"},
|
||||
},
|
||||
{
|
||||
name: "cross numeric comparisons enabled",
|
||||
typeVersionCombinations: []envTypeAndVersion{
|
||||
{version.MajorMinor(1, 28), NewExpressions},
|
||||
{version.MajorMinor(1, 27), StoredExpressions},
|
||||
},
|
||||
validExpressions: []string{"1.5 > 1"},
|
||||
},
|
||||
{
|
||||
name: "user defined variable disabled",
|
||||
typeVersionCombinations: []envTypeAndVersion{
|
||||
{version.MajorMinor(1, 27), NewExpressions},
|
||||
// always enabled for StoredExpressions
|
||||
},
|
||||
invalidExpressions: []string{"fizz == 'buzz'"},
|
||||
activation: map[string]any{"fizz": "buzz"},
|
||||
opts: []VersionedOptions{
|
||||
{IntroducedVersion: version.MajorMinor(1, 28), EnvOptions: []cel.EnvOption{cel.Variable("fizz", cel.StringType)}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "user defined variable enabled",
|
||||
typeVersionCombinations: []envTypeAndVersion{
|
||||
{version.MajorMinor(1, 28), NewExpressions},
|
||||
{version.MajorMinor(1, 27), StoredExpressions},
|
||||
},
|
||||
validExpressions: []string{"fizz == 'buzz'"},
|
||||
activation: map[string]any{"fizz": "buzz"},
|
||||
opts: []VersionedOptions{
|
||||
{IntroducedVersion: version.MajorMinor(1, 28), EnvOptions: []cel.EnvOption{cel.Variable("fizz", cel.StringType)}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "declared type enabled before removed",
|
||||
typeVersionCombinations: []envTypeAndVersion{
|
||||
{version.MajorMinor(1, 28), NewExpressions},
|
||||
// always disabled for StoredExpressions
|
||||
},
|
||||
validExpressions: []string{"widget.x == 'buzz'"},
|
||||
activation: map[string]any{"widget": map[string]any{"x": "buzz"}},
|
||||
opts: []VersionedOptions{
|
||||
{
|
||||
IntroducedVersion: version.MajorMinor(1, 28),
|
||||
RemovedVersion: version.MajorMinor(1, 29),
|
||||
DeclTypes: []*apiservercel.DeclType{widgetsType},
|
||||
EnvOptions: []cel.EnvOption{
|
||||
cel.Variable("widget", cel.ObjectType("Widget")),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "declared type disabled after removed",
|
||||
typeVersionCombinations: []envTypeAndVersion{
|
||||
{version.MajorMinor(1, 29), NewExpressions},
|
||||
{version.MajorMinor(1, 29), StoredExpressions},
|
||||
},
|
||||
invalidExpressions: []string{"widget.x == 'buzz'"},
|
||||
activation: map[string]any{"widget": map[string]any{"x": "buzz"}},
|
||||
opts: []VersionedOptions{
|
||||
{
|
||||
IntroducedVersion: version.MajorMinor(1, 28),
|
||||
RemovedVersion: version.MajorMinor(1, 29),
|
||||
DeclTypes: []*apiservercel.DeclType{widgetsType},
|
||||
EnvOptions: []cel.EnvOption{
|
||||
cel.Variable("widget", cel.ObjectType("Widget")),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "declared type disabled",
|
||||
typeVersionCombinations: []envTypeAndVersion{
|
||||
{version.MajorMinor(1, 27), NewExpressions},
|
||||
// always enabled for StoredExpressions
|
||||
},
|
||||
invalidExpressions: []string{"widget.x == 'buzz'"},
|
||||
activation: map[string]any{"widget": map[string]any{"x": "buzz"}},
|
||||
opts: []VersionedOptions{
|
||||
{
|
||||
IntroducedVersion: version.MajorMinor(1, 28),
|
||||
DeclTypes: []*apiservercel.DeclType{widgetsType},
|
||||
EnvOptions: []cel.EnvOption{
|
||||
cel.Variable("widget", widgetsType.CelType()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "declared type enabled",
|
||||
typeVersionCombinations: []envTypeAndVersion{
|
||||
{version.MajorMinor(1, 28), NewExpressions},
|
||||
{version.MajorMinor(1, 27), StoredExpressions},
|
||||
},
|
||||
validExpressions: []string{"widget.x == 'buzz'"},
|
||||
activation: map[string]any{"widget": map[string]any{"x": "buzz"}},
|
||||
opts: []VersionedOptions{
|
||||
{
|
||||
IntroducedVersion: version.MajorMinor(1, 28),
|
||||
DeclTypes: []*apiservercel.DeclType{widgetsType},
|
||||
EnvOptions: []cel.EnvOption{
|
||||
cel.Variable("widget", widgetsType.CelType()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "library version 0 enabled, version 1 disabled",
|
||||
typeVersionCombinations: []envTypeAndVersion{
|
||||
{version.MajorMinor(1, 27), NewExpressions},
|
||||
// version 1 always enabled for StoredExpressions
|
||||
},
|
||||
validExpressions: []string{"test() == true"},
|
||||
invalidExpressions: []string{"testV1() == true"},
|
||||
opts: []VersionedOptions{
|
||||
{
|
||||
IntroducedVersion: version.MajorMinor(1, 27),
|
||||
RemovedVersion: version.MajorMinor(1, 28),
|
||||
EnvOptions: []cel.EnvOption{
|
||||
library.Test(library.TestVersion(0)),
|
||||
},
|
||||
},
|
||||
{
|
||||
IntroducedVersion: version.MajorMinor(1, 28),
|
||||
EnvOptions: []cel.EnvOption{
|
||||
library.Test(library.TestVersion(1)),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "library version 0 disabled, version 1 enabled",
|
||||
typeVersionCombinations: []envTypeAndVersion{
|
||||
{version.MajorMinor(1, 28), NewExpressions},
|
||||
{version.MajorMinor(1, 26), StoredExpressions},
|
||||
{version.MajorMinor(1, 27), StoredExpressions},
|
||||
{version.MajorMinor(1, 28), StoredExpressions},
|
||||
},
|
||||
validExpressions: []string{"test() == false", "testV1() == true"},
|
||||
opts: []VersionedOptions{
|
||||
{
|
||||
IntroducedVersion: version.MajorMinor(1, 27),
|
||||
RemovedVersion: version.MajorMinor(1, 28),
|
||||
EnvOptions: []cel.EnvOption{
|
||||
library.Test(library.TestVersion(0)),
|
||||
},
|
||||
},
|
||||
{
|
||||
IntroducedVersion: version.MajorMinor(1, 28),
|
||||
EnvOptions: []cel.EnvOption{
|
||||
library.Test(library.TestVersion(1)),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
activation := tc.activation
|
||||
if activation == nil {
|
||||
activation = map[string]any{}
|
||||
}
|
||||
for _, tv := range tc.typeVersionCombinations {
|
||||
t.Run(fmt.Sprintf("version=%s,envType=%s", tv.version.String(), tv.envType), func(t *testing.T) {
|
||||
|
||||
envSet := MustBaseEnvSet(tv.version)
|
||||
if tc.opts != nil {
|
||||
var err error
|
||||
envSet, err = envSet.Extend(tc.opts...)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error extending environment %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
envType := NewExpressions
|
||||
if len(tv.envType) > 0 {
|
||||
envType = tv.envType
|
||||
}
|
||||
|
||||
validationEnv, err := envSet.Env(envType)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, valid := range tc.validExpressions {
|
||||
if ok, err := isValid(validationEnv, valid, activation); !ok {
|
||||
if err != nil {
|
||||
t.Errorf("expected expression to be valid but got %v", err)
|
||||
}
|
||||
t.Error("expected expression to return true")
|
||||
}
|
||||
}
|
||||
for _, invalid := range tc.invalidExpressions {
|
||||
if ok, _ := isValid(validationEnv, invalid, activation); ok {
|
||||
t.Errorf("expected invalid expression to result in error")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func isValid(env *cel.Env, expr string, activation any) (bool, error) {
|
||||
ast, issues := env.Compile(expr)
|
||||
if len(issues.Errors()) > 0 {
|
||||
return false, issues.Err()
|
||||
}
|
||||
prog, err := env.Program(ast)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
result, _, err := prog.Eval(activation)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return result.Value() == true, nil
|
||||
}
|
||||
|
||||
type fakeAuthorizer struct {
|
||||
decision authorizer.Decision
|
||||
reason string
|
||||
err error
|
||||
}
|
||||
|
||||
func (f fakeAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||
return f.decision, f.reason, f.err
|
||||
}
|
||||
@@ -362,7 +362,13 @@ func TestAuthzLibrary(t *testing.T) {
|
||||
|
||||
func testCost(t *testing.T, expr string, expectEsimatedCost checker.CostEstimate, expectRuntimeCost uint64) {
|
||||
est := &CostEstimator{SizeEstimator: &testCostEstimator{}}
|
||||
env, err := cel.NewEnv(append(k8sExtensionLibs, ext.Strings())...)
|
||||
env, err := cel.NewEnv(
|
||||
ext.Strings(),
|
||||
URLs(),
|
||||
Regex(),
|
||||
Lists(),
|
||||
Authz(),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
/*
|
||||
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 library
|
||||
|
||||
import (
|
||||
"github.com/google/cel-go/cel"
|
||||
"github.com/google/cel-go/ext"
|
||||
"github.com/google/cel-go/interpreter"
|
||||
)
|
||||
|
||||
// ExtensionLibs declares the set of CEL extension libraries available everywhere CEL is used in Kubernetes.
|
||||
var ExtensionLibs = append(k8sExtensionLibs, ext.Strings())
|
||||
|
||||
var k8sExtensionLibs = []cel.EnvOption{
|
||||
URLs(),
|
||||
Regex(),
|
||||
Lists(),
|
||||
Authz(),
|
||||
}
|
||||
|
||||
var ExtensionLibRegexOptimizations = []*interpreter.RegexOptimization{FindRegexOptimization, FindAllRegexOptimization}
|
||||
@@ -20,22 +20,16 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/cel-go/cel"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
)
|
||||
|
||||
func TestLibraryCompatibility(t *testing.T) {
|
||||
functionNames := map[string]struct{}{}
|
||||
|
||||
decls := map[cel.Library]map[string][]cel.FunctionOpt{
|
||||
urlsLib: urlLibraryDecls,
|
||||
listsLib: listsLibraryDecls,
|
||||
regexLib: regexLibraryDecls,
|
||||
authzLib: authzLibraryDecls,
|
||||
}
|
||||
if len(k8sExtensionLibs) != len(decls) {
|
||||
t.Errorf("Expected the same number of libraries in the ExtensionLibs as are tested for compatibility")
|
||||
}
|
||||
for _, decl := range decls {
|
||||
for name := range decl {
|
||||
var libs []map[string][]cel.FunctionOpt
|
||||
libs = append(libs, authzLibraryDecls, listsLibraryDecls, regexLibraryDecls, urlLibraryDecls)
|
||||
functionNames := sets.New[string]()
|
||||
for _, lib := range libs {
|
||||
for name := range lib {
|
||||
functionNames[name] = struct{}{}
|
||||
}
|
||||
}
|
||||
@@ -43,19 +37,24 @@ func TestLibraryCompatibility(t *testing.T) {
|
||||
// WARN: All library changes must follow
|
||||
// https://github.com/kubernetes/enhancements/tree/master/keps/sig-api-machinery/2876-crd-validation-expression-language#function-library-updates
|
||||
// and must track the functions here along with which Kubernetes version introduced them.
|
||||
knownFunctions := []string{
|
||||
knownFunctions := sets.New[string](
|
||||
// Kubernetes 1.24:
|
||||
"isSorted", "sum", "max", "min", "indexOf", "lastIndexOf", "find", "findAll", "url", "getScheme", "getHost", "getHostname",
|
||||
"getPort", "getEscapedPath", "getQuery", "isURL",
|
||||
// Kubernetes <1.27>:
|
||||
"path", "group", "serviceAccount", "resource", "subresource", "namespace", "name", "check", "allowed", "denied", "reason",
|
||||
"path", "group", "serviceAccount", "resource", "subresource", "namespace", "name", "check", "allowed", "reason",
|
||||
// Kubernetes <1.??>:
|
||||
}
|
||||
for _, fn := range knownFunctions {
|
||||
delete(functionNames, fn)
|
||||
}
|
||||
)
|
||||
|
||||
if len(functionNames) != 0 {
|
||||
t.Errorf("Expected all functions in the libraries to be assigned to a kubernetes release, but found the unassigned function names: %v", functionNames)
|
||||
// TODO: test celgo function lists
|
||||
|
||||
unexpected := functionNames.Difference(knownFunctions)
|
||||
missing := knownFunctions.Difference(functionNames)
|
||||
|
||||
if len(unexpected) != 0 {
|
||||
t.Errorf("Expected all functions in the libraries to be assigned to a kubernetes release, but found the unexpected function names: %v", unexpected)
|
||||
}
|
||||
if len(missing) != 0 {
|
||||
t.Errorf("Expected all functions in the libraries to be assigned to a kubernetes release, but found the missing function names: %v", missing)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +77,9 @@ func (*regex) CompileOptions() []cel.EnvOption {
|
||||
}
|
||||
|
||||
func (*regex) ProgramOptions() []cel.ProgramOption {
|
||||
return []cel.ProgramOption{}
|
||||
return []cel.ProgramOption{
|
||||
cel.OptimizeRegex(FindRegexOptimization, FindAllRegexOptimization),
|
||||
}
|
||||
}
|
||||
|
||||
func find(strVal ref.Val, regexVal ref.Val) ref.Val {
|
||||
|
||||
79
staging/src/k8s.io/apiserver/pkg/cel/library/test.go
Normal file
79
staging/src/k8s.io/apiserver/pkg/cel/library/test.go
Normal file
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
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 library
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/google/cel-go/cel"
|
||||
"github.com/google/cel-go/common/types"
|
||||
"github.com/google/cel-go/common/types/ref"
|
||||
)
|
||||
|
||||
// Test provides a test() function that returns true.
|
||||
func Test(options ...TestOption) cel.EnvOption {
|
||||
t := &testLib{version: math.MaxUint32}
|
||||
for _, o := range options {
|
||||
t = o(t)
|
||||
}
|
||||
return cel.Lib(t)
|
||||
}
|
||||
|
||||
type testLib struct {
|
||||
version uint32
|
||||
}
|
||||
|
||||
type TestOption func(*testLib) *testLib
|
||||
|
||||
func TestVersion(version uint32) func(lib *testLib) *testLib {
|
||||
return func(sl *testLib) *testLib {
|
||||
sl.version = version
|
||||
return sl
|
||||
}
|
||||
}
|
||||
|
||||
func (t *testLib) CompileOptions() []cel.EnvOption {
|
||||
var options []cel.EnvOption
|
||||
|
||||
if t.version == 0 {
|
||||
options = append(options, cel.Function("test",
|
||||
cel.Overload("test", []*cel.Type{}, cel.BoolType,
|
||||
cel.FunctionBinding(func(args ...ref.Val) ref.Val {
|
||||
return types.True
|
||||
}))))
|
||||
}
|
||||
|
||||
if t.version >= 1 {
|
||||
options = append(options, cel.Function("test",
|
||||
cel.Overload("test", []*cel.Type{}, cel.BoolType,
|
||||
cel.FunctionBinding(func(args ...ref.Val) ref.Val {
|
||||
// Return false here so tests can observe which version of the function is registered
|
||||
// Actual function libraries must not break backward compatibility
|
||||
return types.False
|
||||
}))))
|
||||
options = append(options, cel.Function("testV1",
|
||||
cel.Overload("testV1", []*cel.Type{}, cel.BoolType,
|
||||
cel.FunctionBinding(func(args ...ref.Val) ref.Val {
|
||||
return types.True
|
||||
}))))
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
func (*testLib) ProgramOptions() []cel.ProgramOption {
|
||||
return []cel.ProgramOption{}
|
||||
}
|
||||
@@ -21,12 +21,12 @@ import (
|
||||
|
||||
"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/apimachinery/pkg/util/version"
|
||||
apiservercel "k8s.io/apiserver/pkg/cel"
|
||||
"k8s.io/apiserver/pkg/cel/common"
|
||||
"k8s.io/apiserver/pkg/cel/library"
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
"k8s.io/kube-openapi/pkg/validation/spec"
|
||||
)
|
||||
|
||||
@@ -103,43 +103,26 @@ func TestMultipleTypes(t *testing.T) {
|
||||
// foo is an object with a string field "foo", an integer field "common", and a string field "confusion"
|
||||
// bar is an object with a string field "bar", an integer field "common", and an integer field "confusion"
|
||||
func buildTestEnv() (*cel.Env, error) {
|
||||
var opts []cel.EnvOption
|
||||
opts = append(opts, cel.HomogeneousAggregateLiterals())
|
||||
opts = append(opts, cel.EagerlyValidateDeclarations(true), cel.DefaultUTCTimeZone(true))
|
||||
opts = append(opts, library.ExtensionLibs...)
|
||||
env, err := cel.NewEnv(opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reg := apiservercel.NewRegistry(env)
|
||||
fooType := common.SchemaDeclType(simpleMapSchema("foo", spec.StringProperty()), true).MaybeAssignTypeName("fooType")
|
||||
barType := common.SchemaDeclType(simpleMapSchema("bar", spec.Int64Property()), true).MaybeAssignTypeName("barType")
|
||||
|
||||
declType := common.SchemaDeclType(simpleMapSchema("foo", spec.StringProperty()), true)
|
||||
fooRT, err := apiservercel.NewRuleTypes("fooType", declType, reg)
|
||||
env, err := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()).Extend(
|
||||
environment.VersionedOptions{
|
||||
IntroducedVersion: version.MajorMinor(1, 26),
|
||||
EnvOptions: []cel.EnvOption{
|
||||
cel.Variable("foo", fooType.CelType()),
|
||||
cel.Variable("bar", barType.CelType()),
|
||||
},
|
||||
DeclTypes: []*apiservercel.DeclType{
|
||||
fooType,
|
||||
barType,
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fooRT, err = fooRT.WithTypeProvider(env.TypeProvider())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fooType, _ := fooRT.FindDeclType("fooType")
|
||||
|
||||
declType = common.SchemaDeclType(simpleMapSchema("bar", spec.Int64Property()), true)
|
||||
barRT, err := apiservercel.NewRuleTypes("barType", declType, reg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
barRT, err = barRT.WithTypeProvider(env.TypeProvider())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
barType, _ := barRT.FindDeclType("barType")
|
||||
|
||||
opts = append(opts, cel.CustomTypeProvider(&apiservercel.CompositedTypeProvider{Providers: []ref.TypeProvider{fooRT, barRT}}))
|
||||
opts = append(opts, cel.CustomTypeAdapter(&apiservercel.CompositedTypeAdapter{Adapters: []ref.TypeAdapter{fooRT, barRT}}))
|
||||
opts = append(opts, cel.Variable("foo", fooType.CelType()))
|
||||
opts = append(opts, cel.Variable("bar", barType.CelType()))
|
||||
return env.Extend(opts...)
|
||||
return env.Env(environment.NewExpressions)
|
||||
}
|
||||
|
||||
func simpleMapSchema(fieldName string, confusionSchema *spec.Schema) common.Schema {
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
/*
|
||||
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 (
|
||||
"sync"
|
||||
|
||||
"github.com/google/cel-go/cel"
|
||||
)
|
||||
|
||||
// Resolver declares methods to find policy templates and related configuration objects.
|
||||
type Resolver interface {
|
||||
// FindType returns a DeclType instance corresponding to the given fully-qualified name, if
|
||||
// present.
|
||||
FindType(name string) (*DeclType, bool)
|
||||
}
|
||||
|
||||
// NewRegistry create a registry for keeping track of environments and types
|
||||
// from a base cel.Env expression environment.
|
||||
func NewRegistry(stdExprEnv *cel.Env) *Registry {
|
||||
return &Registry{
|
||||
exprEnvs: map[string]*cel.Env{"": stdExprEnv},
|
||||
types: map[string]*DeclType{
|
||||
BoolType.TypeName(): BoolType,
|
||||
BytesType.TypeName(): BytesType,
|
||||
DoubleType.TypeName(): DoubleType,
|
||||
DurationType.TypeName(): DurationType,
|
||||
IntType.TypeName(): IntType,
|
||||
NullType.TypeName(): NullType,
|
||||
StringType.TypeName(): StringType,
|
||||
TimestampType.TypeName(): TimestampType,
|
||||
UintType.TypeName(): UintType,
|
||||
ListType.TypeName(): ListType,
|
||||
MapType.TypeName(): MapType,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Registry defines a repository of environment, schema, template, and type definitions.
|
||||
//
|
||||
// Registry instances are concurrency-safe.
|
||||
type Registry struct {
|
||||
rwMux sync.RWMutex
|
||||
exprEnvs map[string]*cel.Env
|
||||
types map[string]*DeclType
|
||||
}
|
||||
|
||||
// FindType implements the Resolver interface method.
|
||||
func (r *Registry) FindType(name string) (*DeclType, bool) {
|
||||
r.rwMux.RLock()
|
||||
defer r.rwMux.RUnlock()
|
||||
typ, found := r.types[name]
|
||||
if found {
|
||||
return typ, true
|
||||
}
|
||||
return typ, found
|
||||
}
|
||||
|
||||
// SetType registers a DeclType descriptor by its fully qualified name.
|
||||
func (r *Registry) SetType(name string, declType *DeclType) error {
|
||||
r.rwMux.Lock()
|
||||
defer r.rwMux.Unlock()
|
||||
r.types[name] = declType
|
||||
return nil
|
||||
}
|
||||
@@ -319,44 +319,53 @@ func (f *DeclField) EnumValues() []ref.Val {
|
||||
return ev
|
||||
}
|
||||
|
||||
// NewRuleTypes returns an Open API Schema-based type-system which is CEL compatible.
|
||||
func NewRuleTypes(kind string,
|
||||
declType *DeclType,
|
||||
res Resolver) (*RuleTypes, error) {
|
||||
func allTypesForDecl(declTypes []*DeclType) map[string]*DeclType {
|
||||
if declTypes == nil {
|
||||
return nil
|
||||
}
|
||||
allTypes := map[string]*DeclType{}
|
||||
for _, declType := range declTypes {
|
||||
for k, t := range FieldTypeMap(declType.TypeName(), declType) {
|
||||
allTypes[k] = t
|
||||
}
|
||||
}
|
||||
|
||||
return allTypes
|
||||
}
|
||||
|
||||
// NewDeclTypeProvider returns an Open API Schema-based type-system which is CEL compatible.
|
||||
func NewDeclTypeProvider(rootTypes ...*DeclType) *DeclTypeProvider {
|
||||
// Note, if the schema indicates that it's actually based on another proto
|
||||
// then prefer the proto definition. For expressions in the proto, a new field
|
||||
// annotation will be needed to indicate the expected environment and type of
|
||||
// the expression.
|
||||
schemaTypes, err := newSchemaTypeProvider(kind, declType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
allTypes := allTypesForDecl(rootTypes)
|
||||
return &DeclTypeProvider{
|
||||
registeredTypes: allTypes,
|
||||
}
|
||||
if schemaTypes == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return &RuleTypes{
|
||||
ruleSchemaDeclTypes: schemaTypes,
|
||||
resolver: res,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RuleTypes extends the CEL ref.TypeProvider interface and provides an Open API Schema-based
|
||||
// DeclTypeProvider extends the CEL ref.TypeProvider interface and provides an Open API Schema-based
|
||||
// type-system.
|
||||
type RuleTypes struct {
|
||||
ref.TypeProvider
|
||||
ruleSchemaDeclTypes *schemaTypeProvider
|
||||
typeAdapter ref.TypeAdapter
|
||||
resolver Resolver
|
||||
type DeclTypeProvider struct {
|
||||
registeredTypes map[string]*DeclType
|
||||
typeProvider ref.TypeProvider
|
||||
typeAdapter ref.TypeAdapter
|
||||
}
|
||||
|
||||
func (rt *DeclTypeProvider) EnumValue(enumName string) ref.Val {
|
||||
return rt.typeProvider.EnumValue(enumName)
|
||||
}
|
||||
|
||||
func (rt *DeclTypeProvider) FindIdent(identName string) (ref.Val, bool) {
|
||||
return rt.typeProvider.FindIdent(identName)
|
||||
}
|
||||
|
||||
// EnvOptions returns a set of cel.EnvOption values which includes the declaration set
|
||||
// as well as a custom ref.TypeProvider.
|
||||
//
|
||||
// Note, the standard declaration set includes 'rule' which is defined as the top-level rule-schema
|
||||
// type if one is configured.
|
||||
//
|
||||
// If the RuleTypes value is nil, an empty []cel.EnvOption set is returned.
|
||||
func (rt *RuleTypes) EnvOptions(tp ref.TypeProvider) ([]cel.EnvOption, error) {
|
||||
// If the DeclTypeProvider value is nil, an empty []cel.EnvOption set is returned.
|
||||
func (rt *DeclTypeProvider) EnvOptions(tp ref.TypeProvider) ([]cel.EnvOption, error) {
|
||||
if rt == nil {
|
||||
return []cel.EnvOption{}, nil
|
||||
}
|
||||
@@ -367,13 +376,12 @@ func (rt *RuleTypes) EnvOptions(tp ref.TypeProvider) ([]cel.EnvOption, error) {
|
||||
return []cel.EnvOption{
|
||||
cel.CustomTypeProvider(rtWithTypes),
|
||||
cel.CustomTypeAdapter(rtWithTypes),
|
||||
cel.Variable("rule", rt.ruleSchemaDeclTypes.root.CelType()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// WithTypeProvider returns a new RuleTypes that sets the given TypeProvider
|
||||
// If the original RuleTypes is nil, the returned RuleTypes is still nil.
|
||||
func (rt *RuleTypes) WithTypeProvider(tp ref.TypeProvider) (*RuleTypes, error) {
|
||||
// WithTypeProvider returns a new DeclTypeProvider that sets the given TypeProvider
|
||||
// If the original DeclTypeProvider is nil, the returned DeclTypeProvider is still nil.
|
||||
func (rt *DeclTypeProvider) WithTypeProvider(tp ref.TypeProvider) (*DeclTypeProvider, error) {
|
||||
if rt == nil {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -382,13 +390,12 @@ func (rt *RuleTypes) WithTypeProvider(tp ref.TypeProvider) (*RuleTypes, error) {
|
||||
if ok {
|
||||
ta = tpa
|
||||
}
|
||||
rtWithTypes := &RuleTypes{
|
||||
TypeProvider: tp,
|
||||
typeAdapter: ta,
|
||||
ruleSchemaDeclTypes: rt.ruleSchemaDeclTypes,
|
||||
resolver: rt.resolver,
|
||||
rtWithTypes := &DeclTypeProvider{
|
||||
typeProvider: tp,
|
||||
typeAdapter: ta,
|
||||
registeredTypes: rt.registeredTypes,
|
||||
}
|
||||
for name, declType := range rt.ruleSchemaDeclTypes.types {
|
||||
for name, declType := range rt.registeredTypes {
|
||||
tpType, found := tp.FindType(name)
|
||||
expT, err := declType.ExprType()
|
||||
if err != nil {
|
||||
@@ -396,7 +403,7 @@ func (rt *RuleTypes) WithTypeProvider(tp ref.TypeProvider) (*RuleTypes, error) {
|
||||
}
|
||||
if found && !proto.Equal(tpType, expT) {
|
||||
return nil, fmt.Errorf(
|
||||
"type %s definition differs between CEL environment and rule", name)
|
||||
"type %s definition differs between CEL environment and type provider", name)
|
||||
}
|
||||
}
|
||||
return rtWithTypes, nil
|
||||
@@ -409,7 +416,7 @@ func (rt *RuleTypes) WithTypeProvider(tp ref.TypeProvider) (*RuleTypes, error) {
|
||||
//
|
||||
// Note, when the type name is based on the Open API Schema, the name will reflect the object path
|
||||
// where the type definition appears.
|
||||
func (rt *RuleTypes) FindType(typeName string) (*exprpb.Type, bool) {
|
||||
func (rt *DeclTypeProvider) FindType(typeName string) (*exprpb.Type, bool) {
|
||||
if rt == nil {
|
||||
return nil, false
|
||||
}
|
||||
@@ -421,11 +428,11 @@ func (rt *RuleTypes) FindType(typeName string) (*exprpb.Type, bool) {
|
||||
}
|
||||
return expT, found
|
||||
}
|
||||
return rt.TypeProvider.FindType(typeName)
|
||||
return rt.typeProvider.FindType(typeName)
|
||||
}
|
||||
|
||||
// FindDeclType returns the CPT type description which can be mapped to a CEL type.
|
||||
func (rt *RuleTypes) FindDeclType(typeName string) (*DeclType, bool) {
|
||||
func (rt *DeclTypeProvider) FindDeclType(typeName string) (*DeclType, bool) {
|
||||
if rt == nil {
|
||||
return nil, false
|
||||
}
|
||||
@@ -438,10 +445,10 @@ func (rt *RuleTypes) FindDeclType(typeName string) (*DeclType, bool) {
|
||||
// If, in the future an object instance rather than a type name were provided, the field
|
||||
// resolution might more accurately reflect the expected type model. However, in this case
|
||||
// concessions were made to align with the existing CEL interfaces.
|
||||
func (rt *RuleTypes) FindFieldType(typeName, fieldName string) (*ref.FieldType, bool) {
|
||||
func (rt *DeclTypeProvider) FindFieldType(typeName, fieldName string) (*ref.FieldType, bool) {
|
||||
st, found := rt.findDeclType(typeName)
|
||||
if !found {
|
||||
return rt.TypeProvider.FindFieldType(typeName, fieldName)
|
||||
return rt.typeProvider.FindFieldType(typeName, fieldName)
|
||||
}
|
||||
|
||||
f, found := st.Fields[fieldName]
|
||||
@@ -471,48 +478,63 @@ func (rt *RuleTypes) FindFieldType(typeName, fieldName string) (*ref.FieldType,
|
||||
|
||||
// NativeToValue is an implementation of the ref.TypeAdapater interface which supports conversion
|
||||
// of rule values to CEL ref.Val instances.
|
||||
func (rt *RuleTypes) NativeToValue(val interface{}) ref.Val {
|
||||
func (rt *DeclTypeProvider) NativeToValue(val interface{}) ref.Val {
|
||||
return rt.typeAdapter.NativeToValue(val)
|
||||
}
|
||||
|
||||
// TypeNames returns the list of type names declared within the RuleTypes object.
|
||||
func (rt *RuleTypes) TypeNames() []string {
|
||||
typeNames := make([]string, len(rt.ruleSchemaDeclTypes.types))
|
||||
func (rt *DeclTypeProvider) NewValue(typeName string, fields map[string]ref.Val) ref.Val {
|
||||
// TODO: implement for OpenAPI types to enable CEL object instantiation, which is needed
|
||||
// for mutating admission.
|
||||
return rt.typeProvider.NewValue(typeName, fields)
|
||||
}
|
||||
|
||||
// TypeNames returns the list of type names declared within the DeclTypeProvider object.
|
||||
func (rt *DeclTypeProvider) TypeNames() []string {
|
||||
typeNames := make([]string, len(rt.registeredTypes))
|
||||
i := 0
|
||||
for name := range rt.ruleSchemaDeclTypes.types {
|
||||
for name := range rt.registeredTypes {
|
||||
typeNames[i] = name
|
||||
i++
|
||||
}
|
||||
return typeNames
|
||||
}
|
||||
|
||||
func (rt *RuleTypes) findDeclType(typeName string) (*DeclType, bool) {
|
||||
declType, found := rt.ruleSchemaDeclTypes.types[typeName]
|
||||
func (rt *DeclTypeProvider) findDeclType(typeName string) (*DeclType, bool) {
|
||||
declType, found := rt.registeredTypes[typeName]
|
||||
if found {
|
||||
return declType, true
|
||||
}
|
||||
declType, found = rt.resolver.FindType(typeName)
|
||||
if found {
|
||||
return declType, true
|
||||
}
|
||||
return nil, false
|
||||
declType = findScalar(typeName)
|
||||
return declType, declType != nil
|
||||
}
|
||||
|
||||
func newSchemaTypeProvider(kind string, declType *DeclType) (*schemaTypeProvider, error) {
|
||||
if declType == nil {
|
||||
return nil, nil
|
||||
func findScalar(typename string) *DeclType {
|
||||
switch typename {
|
||||
case BoolType.TypeName():
|
||||
return BoolType
|
||||
case BytesType.TypeName():
|
||||
return BytesType
|
||||
case DoubleType.TypeName():
|
||||
return DoubleType
|
||||
case DurationType.TypeName():
|
||||
return DurationType
|
||||
case IntType.TypeName():
|
||||
return IntType
|
||||
case NullType.TypeName():
|
||||
return NullType
|
||||
case StringType.TypeName():
|
||||
return StringType
|
||||
case TimestampType.TypeName():
|
||||
return TimestampType
|
||||
case UintType.TypeName():
|
||||
return UintType
|
||||
case ListType.TypeName():
|
||||
return ListType
|
||||
case MapType.TypeName():
|
||||
return MapType
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
root := declType.MaybeAssignTypeName(kind)
|
||||
types := FieldTypeMap(kind, root)
|
||||
return &schemaTypeProvider{
|
||||
root: root,
|
||||
types: types,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type schemaTypeProvider struct {
|
||||
root *DeclType
|
||||
types map[string]*DeclType
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
@@ -26,6 +26,8 @@ import (
|
||||
"github.com/google/cel-go/cel"
|
||||
"github.com/google/cel-go/interpreter"
|
||||
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
|
||||
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
@@ -40,16 +42,16 @@ import (
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
commoncel "k8s.io/apiserver/pkg/cel"
|
||||
"k8s.io/apiserver/pkg/cel/library"
|
||||
celopenapi "k8s.io/apiserver/pkg/cel/openapi"
|
||||
"k8s.io/apiserver/pkg/cel/openapi/resolver"
|
||||
k8sscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/kube-openapi/pkg/validation/spec"
|
||||
"k8s.io/utils/pointer"
|
||||
|
||||
apiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
||||
corev1 "k8s.io/kubernetes/pkg/apis/core/v1"
|
||||
"k8s.io/kubernetes/pkg/generated/openapi"
|
||||
"k8s.io/kubernetes/test/integration/framework"
|
||||
"k8s.io/utils/pointer"
|
||||
)
|
||||
|
||||
func TestTypeResolver(t *testing.T) {
|
||||
@@ -365,21 +367,13 @@ func TestBuiltinResolution(t *testing.T) {
|
||||
// with the practical defaults.
|
||||
// `self` is defined as the object being evaluated against.
|
||||
func simpleCompileCEL(schema *spec.Schema, expression string) (cel.Program, error) {
|
||||
var opts []cel.EnvOption
|
||||
opts = append(opts, cel.HomogeneousAggregateLiterals())
|
||||
opts = append(opts, cel.EagerlyValidateDeclarations(true), cel.DefaultUTCTimeZone(true))
|
||||
opts = append(opts, library.ExtensionLibs...)
|
||||
env, err := cel.NewEnv(opts...)
|
||||
env, err := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()).Env(environment.NewExpressions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reg := commoncel.NewRegistry(env)
|
||||
declType := celopenapi.SchemaDeclType(schema, true)
|
||||
rt, err := commoncel.NewRuleTypes("selfType", declType, reg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opts, err = rt.EnvOptions(env.TypeProvider())
|
||||
declType := celopenapi.SchemaDeclType(schema, true).MaybeAssignTypeName("selfType")
|
||||
rt := commoncel.NewDeclTypeProvider(declType)
|
||||
opts, err := rt.EnvOptions(env.TypeProvider())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
1
vendor/modules.txt
vendored
1
vendor/modules.txt
vendored
@@ -1464,6 +1464,7 @@ k8s.io/apiserver/pkg/authorization/path
|
||||
k8s.io/apiserver/pkg/authorization/union
|
||||
k8s.io/apiserver/pkg/cel
|
||||
k8s.io/apiserver/pkg/cel/common
|
||||
k8s.io/apiserver/pkg/cel/environment
|
||||
k8s.io/apiserver/pkg/cel/library
|
||||
k8s.io/apiserver/pkg/cel/metrics
|
||||
k8s.io/apiserver/pkg/cel/openapi
|
||||
|
||||
Reference in New Issue
Block a user