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:
parent
9aebe10d15
commit
86fff3b394
4 changed files with 215 additions and 57 deletions
|
@ -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))
|
||||
|
|
|
@ -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,24 +100,52 @@ 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()))
|
||||
}
|
||||
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()))
|
||||
}
|
||||
auditAnnotations[auditAnnotation.Key] = prog
|
||||
errs := compileAuditAnnotations(path, policy.Spec.AuditAnnotations, env, auditAnnotations)
|
||||
if errs != nil {
|
||||
return nil, append(allErrs, errs...)
|
||||
}
|
||||
}
|
||||
|
||||
// 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...)
|
||||
}
|
||||
// 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
|
||||
var polexMatchConditions []cel.Program
|
||||
if len(exceptions) > 0 {
|
||||
|
@ -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{
|
||||
|
|
|
@ -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')",
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Reference in a new issue