1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-06 16:06:56 +00:00

feat: compile and evaluate autogen rules (#12163)

This commit is contained in:
Mariam Fahmy 2025-02-15 06:56:51 +02:00 committed by GitHub
parent 9aebe10d15
commit 86fff3b394
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 215 additions and 57 deletions

View file

@ -5,6 +5,7 @@ import (
"fmt"
policiesv1alpha1 "github.com/kyverno/kyverno/api/policies.kyverno.io/v1alpha1"
vpolautogen "github.com/kyverno/kyverno/pkg/cel/autogen"
contextlib "github.com/kyverno/kyverno/pkg/cel/libs/context"
"github.com/kyverno/kyverno/pkg/cel/matching"
"github.com/kyverno/kyverno/pkg/cel/utils"
@ -151,21 +152,56 @@ func (e *engine) Handle(ctx context.Context, request EngineRequest) (EngineRespo
return response, nil
}
func (e *engine) matchPolicy(policy CompiledPolicy, attr admission.Attributes, namespace runtime.Object) (bool, int, error) {
match := func(constraints *admissionregistrationv1.MatchResources) (bool, error) {
criteria := matchCriteria{constraints: constraints}
matches, err := e.matcher.Match(&criteria, attr, namespace)
if err != nil {
return false, err
}
return matches, nil
}
// match against main policy constraints
matches, err := match(policy.Policy.Spec.MatchConstraints)
if err != nil {
return false, -1, err
}
if matches {
return true, -1, nil
}
// match against autogen rules
autogenRules := vpolautogen.ComputeRules(&policy.Policy)
for i, autogenRule := range autogenRules {
matches, err := match(autogenRule.MatchConstraints)
if err != nil {
return false, -1, err
}
if matches {
return true, i, nil
}
}
return false, -1, nil
}
func (e *engine) handlePolicy(ctx context.Context, policy CompiledPolicy, attr admission.Attributes, request *admissionv1.AdmissionRequest, namespace runtime.Object, context contextlib.ContextInterface) PolicyResponse {
response := PolicyResponse{
Actions: policy.Actions,
Policy: policy.Policy,
}
autogenIndex := -1
if e.matcher != nil {
criteria := matchCriteria{constraints: policy.Policy.Spec.MatchConstraints}
if matches, err := e.matcher.Match(&criteria, attr, namespace); err != nil {
matches, index, err := e.matchPolicy(policy, attr, namespace)
if err != nil {
response.Rules = handlers.WithResponses(engineapi.RuleError("match", engineapi.Validation, "failed to execute matching", err, nil))
return response
} else if !matches {
return response
}
autogenIndex = index
}
results, err := policy.CompiledPolicy.Evaluate(ctx, attr, request, namespace, context)
results, err := policy.CompiledPolicy.Evaluate(ctx, attr, request, namespace, context, autogenIndex)
// TODO: error is about match conditions here ?
if err != nil {
response.Rules = handlers.WithResponses(engineapi.RuleError("evaluation", engineapi.Validation, "failed to load context", err, nil))

View file

@ -7,6 +7,7 @@ import (
"github.com/google/cel-go/common/types"
policiesv1alpha1 "github.com/kyverno/kyverno/api/policies.kyverno.io/v1alpha1"
engine "github.com/kyverno/kyverno/pkg/cel"
vpolautogen "github.com/kyverno/kyverno/pkg/cel/autogen"
"github.com/kyverno/kyverno/pkg/cel/libs/context"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
@ -70,38 +71,18 @@ func (c *compiler) Compile(policy *policiesv1alpha1.ValidatingPolicy, exceptions
matchConditions := make([]cel.Program, 0, len(policy.Spec.MatchConditions))
{
path := path.Child("matchConditions")
for i, matchCondition := range policy.Spec.MatchConditions {
path := path.Index(i).Child("expression")
ast, issues := env.Compile(matchCondition.Expression)
if err := issues.Err(); err != nil {
return nil, append(allErrs, field.Invalid(path, matchCondition.Expression, err.Error()))
}
if !ast.OutputType().IsExactType(types.BoolType) {
msg := fmt.Sprintf("output is expected to be of type %s", types.BoolType.TypeName())
return nil, append(allErrs, field.Invalid(path, matchCondition.Expression, msg))
}
prog, err := env.Program(ast)
if err != nil {
return nil, append(allErrs, field.Invalid(path, matchCondition.Expression, err.Error()))
}
matchConditions = append(matchConditions, prog)
programs, errs := compileMatchConditions(path, policy.Spec.MatchConditions, env)
if errs != nil {
return nil, append(allErrs, errs...)
}
matchConditions = append(matchConditions, programs...)
}
variables := map[string]cel.Program{}
{
path := path.Child("variables")
for i, variable := range policy.Spec.Variables {
path := path.Index(i).Child("expression")
ast, issues := env.Compile(variable.Expression)
if err := issues.Err(); err != nil {
return nil, append(allErrs, field.Invalid(path, variable.Expression, err.Error()))
}
variablesProvider.RegisterField(variable.Name, ast.OutputType())
prog, err := env.Program(ast)
if err != nil {
return nil, append(allErrs, field.Invalid(path, variable.Expression, err.Error()))
}
variables[variable.Name] = prog
errs := compileVariables(path, policy.Spec.Variables, variablesProvider, env, variables)
if errs != nil {
return nil, append(allErrs, errs...)
}
}
validations := make([]compiledValidation, 0, len(policy.Spec.Validations))
@ -119,22 +100,50 @@ func (c *compiler) Compile(policy *policiesv1alpha1.ValidatingPolicy, exceptions
auditAnnotations := map[string]cel.Program{}
{
path := path.Child("auditAnnotations")
for i, auditAnnotation := range policy.Spec.AuditAnnotations {
path := path.Index(i).Child("valueExpression")
ast, issues := env.Compile(auditAnnotation.ValueExpression)
if err := issues.Err(); err != nil {
return nil, append(allErrs, field.Invalid(path, auditAnnotation.ValueExpression, err.Error()))
errs := compileAuditAnnotations(path, policy.Spec.AuditAnnotations, env, auditAnnotations)
if errs != nil {
return nil, append(allErrs, errs...)
}
if !ast.OutputType().IsExactType(types.StringType) && !ast.OutputType().IsExactType(types.NullType) {
msg := fmt.Sprintf("output is expected to be either of type %s or %s", types.StringType.TypeName(), types.NullType.TypeName())
return nil, append(allErrs, field.Invalid(path, auditAnnotation.ValueExpression, msg))
}
prog, err := env.Program(ast)
if err != nil {
return nil, append(allErrs, field.Invalid(path, auditAnnotation.ValueExpression, err.Error()))
// compile autogen rules
autogenPath := field.NewPath("status").Child("autogen").Child("rules")
autogenRules := vpolautogen.ComputeRules(policy)
compiledRules := make([]compiledAutogenRule, 0, len(autogenRules))
for i, rule := range autogenRules {
// compile match conditions
matchConditions, errs := compileMatchConditions(autogenPath.Index(i).Child("matchConditions"), rule.MatchConditions, env)
if errs != nil {
return nil, append(allErrs, errs...)
}
auditAnnotations[auditAnnotation.Key] = prog
// compile variables
variables := map[string]cel.Program{}
errs = compileVariables(autogenPath.Index(i).Child("variables"), rule.Variables, variablesProvider, env, variables)
if errs != nil {
return nil, append(allErrs, errs...)
}
// compile validations
validations := make([]compiledValidation, 0, len(rule.Validations))
for j, rule := range rule.Validations {
path := autogenPath.Index(j).Child("validations")
program, errs := compileValidation(path, rule, env)
if errs != nil {
return nil, append(allErrs, errs...)
}
validations = append(validations, program)
}
// compile audit annotations
auditAnnotations := map[string]cel.Program{}
errs = compileAuditAnnotations(autogenPath.Index(i).Child("auditAnnotations"), rule.AuditAnnotation, env, auditAnnotations)
if errs != nil {
return nil, append(allErrs, errs...)
}
compiledRules = append(compiledRules, compiledAutogenRule{
matchConditions: matchConditions,
variables: variables,
validations: validations,
auditAnnotation: auditAnnotations,
})
}
// exceptions' match conditions
@ -168,9 +177,71 @@ func (c *compiler) Compile(policy *policiesv1alpha1.ValidatingPolicy, exceptions
validations: validations,
auditAnnotations: auditAnnotations,
polexMatchConditions: polexMatchConditions,
autogenRules: compiledRules,
}, nil
}
func compileMatchConditions(path *field.Path, matchConditions []admissionregistrationv1.MatchCondition, env *cel.Env) ([]cel.Program, field.ErrorList) {
var allErrs field.ErrorList
result := make([]cel.Program, 0, len(matchConditions))
for i, matchCondition := range matchConditions {
path := path.Index(i).Child("expression")
ast, issues := env.Compile(matchCondition.Expression)
if err := issues.Err(); err != nil {
return nil, append(allErrs, field.Invalid(path, matchCondition.Expression, err.Error()))
}
if !ast.OutputType().IsExactType(types.BoolType) {
msg := fmt.Sprintf("output is expected to be of type %s", types.BoolType.TypeName())
return nil, append(allErrs, field.Invalid(path, matchCondition.Expression, msg))
}
prog, err := env.Program(ast)
if err != nil {
return nil, append(allErrs, field.Invalid(path, matchCondition.Expression, err.Error()))
}
result = append(result, prog)
}
return result, nil
}
func compileVariables(path *field.Path, variables []admissionregistrationv1.Variable, variablesProvider *variablesProvider, env *cel.Env, result map[string]cel.Program) field.ErrorList {
var allErrs field.ErrorList
for i, variable := range variables {
path := path.Index(i).Child("expression")
ast, issues := env.Compile(variable.Expression)
if err := issues.Err(); err != nil {
return append(allErrs, field.Invalid(path, variable.Expression, err.Error()))
}
variablesProvider.RegisterField(variable.Name, ast.OutputType())
prog, err := env.Program(ast)
if err != nil {
return append(allErrs, field.Invalid(path, variable.Expression, err.Error()))
}
result[variable.Name] = prog
}
return nil
}
func compileAuditAnnotations(path *field.Path, auditAnnotations []admissionregistrationv1.AuditAnnotation, env *cel.Env, result map[string]cel.Program) field.ErrorList {
var allErrs field.ErrorList
for i, auditAnnotation := range auditAnnotations {
path := path.Index(i).Child("valueExpression")
ast, issues := env.Compile(auditAnnotation.ValueExpression)
if err := issues.Err(); err != nil {
return append(allErrs, field.Invalid(path, auditAnnotation.ValueExpression, err.Error()))
}
if !ast.OutputType().IsExactType(types.StringType) && !ast.OutputType().IsExactType(types.NullType) {
msg := fmt.Sprintf("output is expected to be either of type %s or %s", types.StringType.TypeName(), types.NullType.TypeName())
return append(allErrs, field.Invalid(path, auditAnnotation.ValueExpression, msg))
}
prog, err := env.Program(ast)
if err != nil {
return append(allErrs, field.Invalid(path, auditAnnotation.ValueExpression, err.Error()))
}
result[auditAnnotation.Key] = prog
}
return nil
}
func compileValidation(path *field.Path, rule admissionregistrationv1.Validation, env *cel.Env) (compiledValidation, field.ErrorList) {
var allErrs field.ErrorList
compiled := compiledValidation{

View file

@ -26,6 +26,23 @@ func Test_compiler_Compile(t *testing.T) {
},
Spec: policiesv1alpha1.ValidatingPolicySpec{
ValidatingAdmissionPolicySpec: admissionregistrationv1.ValidatingAdmissionPolicySpec{
MatchConstraints: &admissionregistrationv1.MatchResources{
ResourceRules: []admissionregistrationv1.NamedRuleWithOperations{
{
RuleWithOperations: admissionregistrationv1.RuleWithOperations{
Operations: []admissionregistrationv1.OperationType{
admissionregistrationv1.Create,
admissionregistrationv1.Update,
},
Rule: admissionregistrationv1.Rule{
APIGroups: []string{""},
APIVersions: []string{"v1"},
Resources: []string{"pods"},
},
},
},
},
},
Variables: []admissionregistrationv1.Variable{{
Name: "environment",
Expression: "has(object.metadata.labels) && 'env' in object.metadata.labels && object.metadata.labels['env'] == 'prod'",
@ -48,6 +65,23 @@ func Test_compiler_Compile(t *testing.T) {
},
Spec: policiesv1alpha1.ValidatingPolicySpec{
ValidatingAdmissionPolicySpec: admissionregistrationv1.ValidatingAdmissionPolicySpec{
MatchConstraints: &admissionregistrationv1.MatchResources{
ResourceRules: []admissionregistrationv1.NamedRuleWithOperations{
{
RuleWithOperations: admissionregistrationv1.RuleWithOperations{
Operations: []admissionregistrationv1.OperationType{
admissionregistrationv1.Create,
admissionregistrationv1.Update,
},
Rule: admissionregistrationv1.Rule{
APIGroups: []string{""},
APIVersions: []string{"v1"},
Resources: []string{"pods"},
},
},
},
},
},
Variables: []admissionregistrationv1.Variable{{
Name: "cm",
Expression: "context.GetConfigMap('foo', 'bar')",

View file

@ -26,7 +26,7 @@ type EvaluationResult struct {
}
type CompiledPolicy interface {
Evaluate(context.Context, admission.Attributes, *admissionv1.AdmissionRequest, runtime.Object, contextlib.ContextInterface) ([]EvaluationResult, error)
Evaluate(context.Context, admission.Attributes, *admissionv1.AdmissionRequest, runtime.Object, contextlib.ContextInterface, int) ([]EvaluationResult, error)
}
type compiledValidation struct {
@ -35,6 +35,13 @@ type compiledValidation struct {
program cel.Program
}
type compiledAutogenRule struct {
matchConditions []cel.Program
validations []compiledValidation
auditAnnotation map[string]cel.Program
variables map[string]cel.Program
}
type compiledPolicy struct {
failurePolicy admissionregistrationv1.FailurePolicyType
matchConditions []cel.Program
@ -42,6 +49,7 @@ type compiledPolicy struct {
validations []compiledValidation
auditAnnotations map[string]cel.Program
polexMatchConditions []cel.Program
autogenRules []compiledAutogenRule
}
func (p *compiledPolicy) Evaluate(
@ -50,10 +58,11 @@ func (p *compiledPolicy) Evaluate(
request *admissionv1.AdmissionRequest,
namespace runtime.Object,
context contextlib.ContextInterface,
autogenIndex int,
) ([]EvaluationResult, error) {
// check if the resource matches an exception
if len(p.polexMatchConditions) > 0 {
match, err := p.match(ctx, attr, request, namespace, true)
match, err := p.match(ctx, attr, request, namespace, p.polexMatchConditions)
if err != nil {
return nil, err
}
@ -62,7 +71,20 @@ func (p *compiledPolicy) Evaluate(
}
}
match, err := p.match(ctx, attr, request, namespace, false)
var matchConditions []cel.Program
var validations []compiledValidation
var variables map[string]cel.Program
if autogenIndex != -1 {
matchConditions = p.autogenRules[autogenIndex].matchConditions
validations = p.autogenRules[autogenIndex].validations
variables = p.autogenRules[autogenIndex].variables
} else {
matchConditions = p.matchConditions
validations = p.validations
variables = p.variables
}
match, err := p.match(ctx, attr, request, namespace, matchConditions)
if err != nil {
return nil, err
}
@ -94,7 +116,7 @@ func (p *compiledPolicy) Evaluate(
RequestKey: requestVal.Object,
VariablesKey: vars,
}
for name, variable := range p.variables {
for name, variable := range variables {
vars.Append(name, func(*lazy.MapValue) ref.Val {
out, _, err := variable.ContextEval(ctx, data)
if out != nil {
@ -106,8 +128,8 @@ func (p *compiledPolicy) Evaluate(
return nil
})
}
results := make([]EvaluationResult, 0, len(p.validations))
for _, validation := range p.validations {
results := make([]EvaluationResult, 0, len(validations))
for _, validation := range validations {
out, _, err := validation.program.ContextEval(ctx, data)
// evaluate only when rule fails
var message string
@ -137,7 +159,7 @@ func (p *compiledPolicy) match(
attr admission.Attributes,
request *admissionv1.AdmissionRequest,
namespace runtime.Object,
isException bool,
matchConditions []cel.Program,
) (bool, error) {
namespaceVal, err := objectToResolveVal(namespace)
if err != nil {
@ -162,11 +184,6 @@ func (p *compiledPolicy) match(
RequestKey: requestVal.Object,
}
var errs []error
matchConditions := p.matchConditions
if isException {
matchConditions = p.polexMatchConditions
}
for _, matchCondition := range matchConditions {
// evaluate the condition
out, _, err := matchCondition.ContextEval(ctx, data)