diff --git a/pkg/cel/exception/compiler.go b/pkg/cel/exception/compiler.go new file mode 100644 index 0000000000..5aa6f09045 --- /dev/null +++ b/pkg/cel/exception/compiler.go @@ -0,0 +1,58 @@ +package exception + +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" + policy "github.com/kyverno/kyverno/pkg/cel/policy" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +type Compiler interface { + Compile(*kyvernov2alpha1.CELPolicyException) (*CompiledException, field.ErrorList) +} + +func NewCompiler() Compiler { + return &compiler{} +} + +type compiler struct{} + +func (c *compiler) Compile(exception *kyvernov2alpha1.CELPolicyException) (*CompiledException, field.ErrorList) { + var allErrs field.ErrorList + base, err := engine.NewEnv() + if err != nil { + return nil, append(allErrs, field.InternalError(nil, err)) + } + options := []cel.EnvOption{ + cel.Variable(policy.ObjectKey, cel.DynType), + } + env, err := base.Extend(options...) + if err != nil { + return nil, append(allErrs, field.InternalError(nil, err)) + } + path := field.NewPath("spec.matchConditions") + matchConditions := make([]cel.Program, 0, len(exception.Spec.MatchConditions)) + for i, matchCondition := range exception.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) + } + return &CompiledException{ + matchConditions: matchConditions, + }, nil +} diff --git a/pkg/cel/exception/compiler_test.go b/pkg/cel/exception/compiler_test.go new file mode 100644 index 0000000000..9d2549b4ff --- /dev/null +++ b/pkg/cel/exception/compiler_test.go @@ -0,0 +1,51 @@ +package exception + +import ( + "testing" + + kyvernov2alpha1 "github.com/kyverno/kyverno/api/kyverno/v2alpha1" + "github.com/stretchr/testify/assert" + admissionregistrationv1 "k8s.io/api/admissionregistration/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func Test_compiler_Compile(t *testing.T) { + tests := []struct { + name string + exception *kyvernov2alpha1.CELPolicyException + wantErr bool + }{ + { + name: "simple", + exception: &kyvernov2alpha1.CELPolicyException{ + TypeMeta: metav1.TypeMeta{ + APIVersion: kyvernov2alpha1.GroupVersion.String(), + Kind: "CELPolicyException", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "exception", + }, + Spec: kyvernov2alpha1.CELPolicyExceptionSpec{ + MatchConditions: []admissionregistrationv1.MatchCondition{ + { + Name: "check env label", + Expression: "has(object.metadata.labels) && 'env' in object.metadata.labels && object.metadata.labels['env'] == 'prod'", + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := NewCompiler() + compiled, errs := c.Compile(tt.exception) + if tt.wantErr { + assert.Error(t, errs.ToAggregate()) + } else { + assert.NoError(t, errs.ToAggregate()) + assert.NotNil(t, compiled) + } + }) + } +} diff --git a/pkg/cel/exception/exception.go b/pkg/cel/exception/exception.go new file mode 100644 index 0000000000..f7a1a16a10 --- /dev/null +++ b/pkg/cel/exception/exception.go @@ -0,0 +1,9 @@ +package exception + +import ( + "github.com/google/cel-go/cel" +) + +type CompiledException struct { + matchConditions []cel.Program +}