diff --git a/api/kyverno/v2alpha1/validating_policy.go b/api/kyverno/v2alpha1/validating_policy.go index 7ebff90f3a..45154a68a2 100644 --- a/api/kyverno/v2alpha1/validating_policy.go +++ b/api/kyverno/v2alpha1/validating_policy.go @@ -17,6 +17,13 @@ type ValidatingPolicy struct { Spec admissionregistrationv1.ValidatingAdmissionPolicySpec `json:"spec"` } +func (s *ValidatingPolicy) GetFailurePolicy() admissionregistrationv1.FailurePolicyType { + if s.Spec.FailurePolicy == nil { + return admissionregistrationv1.Fail + } + return *s.Spec.FailurePolicy +} + // +kubebuilder:object:root=true // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/cel/policy/compiler.go b/pkg/cel/policy/compiler.go new file mode 100644 index 0000000000..4b3e1d546c --- /dev/null +++ b/pkg/cel/policy/compiler.go @@ -0,0 +1,123 @@ +package policy + +import ( + "fmt" + + "github.com/google/cel-go/cel" + "github.com/google/cel-go/common/types" + kyvernov2alpha1 "github.com/kyverno/kyverno/api/kyverno/v2alpha1" + engine "github.com/kyverno/kyverno/pkg/cel" + "github.com/kyverno/kyverno/pkg/cel/libs/context" + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +const ( + ContextKey = "context" + ObjectKey = "object" + VariablesKey = "variables" +) + +type Compiler interface { + Compile(*kyvernov2alpha1.ValidatingPolicy) (*CompiledPolicy, field.ErrorList) +} + +func NewCompiler() Compiler { + return &compiler{} +} + +type compiler struct{} + +func (c *compiler) Compile(policy *kyvernov2alpha1.ValidatingPolicy) (*CompiledPolicy, field.ErrorList) { + var allErrs field.ErrorList + base, err := engine.NewEnv() + if err != nil { + return nil, append(allErrs, field.InternalError(nil, err)) + } + provider := NewVariablesProvider(base.CELTypeProvider()) + env, err := base.Extend( + cel.Variable(ContextKey, context.ContextType), + cel.Variable(ObjectKey, cel.DynType), + cel.Variable(VariablesKey, VariablesType), + cel.CustomTypeProvider(provider), + ) + if err != nil { + return nil, append(allErrs, field.InternalError(nil, err)) + } + path := field.NewPath("spec") + 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) + } + } + 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())) + } + provider.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 + } + } + validations := make([]cel.Program, 0, len(policy.Spec.Validations)) + { + path := path.Child("validations") + for i, rule := range policy.Spec.Validations { + path := path.Index(i) + program, errs := compileValidation(path, rule, env) + if errs != nil { + return nil, append(allErrs, errs...) + } + validations = append(validations, program) + } + } + return &CompiledPolicy{ + failurePolicy: policy.GetFailurePolicy(), + matchConditions: matchConditions, + variables: variables, + validations: validations, + }, nil +} + +func compileValidation(path *field.Path, rule admissionregistrationv1.Validation, env *cel.Env) (cel.Program, field.ErrorList) { + var allErrs field.ErrorList + { + path := path.Child("expression") + ast, issues := env.Compile(rule.Expression) + if err := issues.Err(); err != nil { + return nil, append(allErrs, field.Invalid(path, rule.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, rule.Expression, msg)) + } + program, err := env.Program(ast) + if err != nil { + return nil, append(allErrs, field.Invalid(path, rule.Expression, err.Error())) + } + return program, nil + } +} diff --git a/pkg/cel/policy/policy.go b/pkg/cel/policy/policy.go new file mode 100644 index 0000000000..5bf19146f7 --- /dev/null +++ b/pkg/cel/policy/policy.go @@ -0,0 +1,13 @@ +package policy + +import ( + "github.com/google/cel-go/cel" + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" +) + +type CompiledPolicy struct { + failurePolicy admissionregistrationv1.FailurePolicyType + matchConditions []cel.Program + variables map[string]cel.Program + validations []cel.Program +} diff --git a/pkg/cel/policy/variables.go b/pkg/cel/policy/variables.go new file mode 100644 index 0000000000..3f3fbf91ff --- /dev/null +++ b/pkg/cel/policy/variables.go @@ -0,0 +1,67 @@ +package policy + +import ( + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" +) + +var ( + VariablesType = types.NewObjectType("kyverno.variables") + variablesTypeType = types.NewTypeTypeWithParam(VariablesType) +) + +type variablesProvider struct { + inner types.Provider + fields map[string]*types.Type + names []string +} + +func NewVariablesProvider(inner types.Provider) *variablesProvider { + return &variablesProvider{ + inner: inner, + fields: make(map[string]*types.Type), + } +} + +func (p *variablesProvider) RegisterField(name string, t *types.Type) { + p.fields[name] = t + p.names = append(p.names, name) +} + +func (p *variablesProvider) EnumValue(enumName string) ref.Val { + return p.inner.EnumValue(enumName) +} + +func (p *variablesProvider) FindIdent(identName string) (ref.Val, bool) { + return p.inner.FindIdent(identName) +} + +func (p *variablesProvider) FindStructType(structType string) (*types.Type, bool) { + if structType == VariablesType.DeclaredTypeName() { + return variablesTypeType, true + } + return p.inner.FindStructType(structType) +} + +func (p *variablesProvider) FindStructFieldNames(structType string) ([]string, bool) { + if structType == VariablesType.DeclaredTypeName() { + return p.names, true + } + return p.inner.FindStructFieldNames(structType) +} + +func (p *variablesProvider) FindStructFieldType(structType, fieldName string) (*types.FieldType, bool) { + if structType == VariablesType.DeclaredTypeName() { + if t, ok := p.fields[fieldName]; ok { + return &types.FieldType{ + Type: t, + }, true + } + return nil, false + } + return p.inner.FindStructFieldType(structType, fieldName) +} + +func (p *variablesProvider) NewValue(structType string, fields map[string]ref.Val) ref.Val { + return p.inner.NewValue(structType, fields) +}