mirror of
https://github.com/kyverno/kyverno.git
synced 2025-03-06 07:57:07 +00:00
feat: add MAP's mutation logic for the CLI (#11946)
Signed-off-by: Mariam Fahmy <mariam.fahmy@nirmata.com>
This commit is contained in:
parent
a915d28b25
commit
77a7e5193a
8 changed files with 845 additions and 35 deletions
|
@ -2,8 +2,11 @@ package admissionpolicy
|
|||
|
||||
import (
|
||||
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
|
||||
"k8s.io/api/admissionregistration/v1alpha1"
|
||||
admissionregistrationv1alpha1 "k8s.io/api/admissionregistration/v1alpha1"
|
||||
admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/policy/validating"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
|
@ -11,16 +14,14 @@ import (
|
|||
|
||||
type Compiler struct {
|
||||
compositedCompiler cel.CompositedCompiler
|
||||
// CEL expressions
|
||||
validateExpressions []admissionregistrationv1beta1.Validation
|
||||
auditAnnotationExpressions []admissionregistrationv1beta1.AuditAnnotation
|
||||
matchExpressions []admissionregistrationv1.MatchCondition
|
||||
variables []admissionregistrationv1beta1.Variable
|
||||
validations []admissionregistrationv1beta1.Validation
|
||||
mutations []admissionregistrationv1alpha1.Mutation
|
||||
auditAnnotations []admissionregistrationv1beta1.AuditAnnotation
|
||||
matchConditions []admissionregistrationv1.MatchCondition
|
||||
variables []admissionregistrationv1beta1.Variable
|
||||
}
|
||||
|
||||
func NewCompiler(
|
||||
validations []admissionregistrationv1beta1.Validation,
|
||||
auditAnnotations []admissionregistrationv1beta1.AuditAnnotation,
|
||||
matchConditions []admissionregistrationv1.MatchCondition,
|
||||
variables []admissionregistrationv1beta1.Variable,
|
||||
) (*Compiler, error) {
|
||||
|
@ -29,14 +30,49 @@ func NewCompiler(
|
|||
return nil, err
|
||||
}
|
||||
return &Compiler{
|
||||
compositedCompiler: *compositedCompiler,
|
||||
validateExpressions: validations,
|
||||
auditAnnotationExpressions: auditAnnotations,
|
||||
matchExpressions: matchConditions,
|
||||
variables: variables,
|
||||
compositedCompiler: *compositedCompiler,
|
||||
matchConditions: matchConditions,
|
||||
variables: variables,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Compiler) WithValidations(validations []admissionregistrationv1beta1.Validation) {
|
||||
c.validations = validations
|
||||
}
|
||||
|
||||
func (c *Compiler) WithMutations(mutations []admissionregistrationv1alpha1.Mutation) {
|
||||
c.mutations = mutations
|
||||
}
|
||||
|
||||
func (c *Compiler) WithAuditAnnotations(auditAnnotations []admissionregistrationv1beta1.AuditAnnotation) {
|
||||
c.auditAnnotations = auditAnnotations
|
||||
}
|
||||
|
||||
func (c Compiler) CompileMutations(patchOptions cel.OptionalVariableDeclarations) []patch.Patcher {
|
||||
var patchers []patch.Patcher
|
||||
for _, m := range c.mutations {
|
||||
switch m.PatchType {
|
||||
case v1alpha1.PatchTypeJSONPatch:
|
||||
if m.JSONPatch != nil {
|
||||
accessor := &patch.JSONPatchCondition{
|
||||
Expression: m.JSONPatch.Expression,
|
||||
}
|
||||
compileResult := c.compositedCompiler.CompileMutatingEvaluator(accessor, patchOptions, environment.StoredExpressions)
|
||||
patchers = append(patchers, patch.NewJSONPatcher(compileResult))
|
||||
}
|
||||
case v1alpha1.PatchTypeApplyConfiguration:
|
||||
if m.ApplyConfiguration != nil {
|
||||
accessor := &patch.ApplyConfigurationCondition{
|
||||
Expression: m.ApplyConfiguration.Expression,
|
||||
}
|
||||
compileResult := c.compositedCompiler.CompileMutatingEvaluator(accessor, patchOptions, environment.StoredExpressions)
|
||||
patchers = append(patchers, patch.NewApplyConfigurationPatcher(compileResult))
|
||||
}
|
||||
}
|
||||
}
|
||||
return patchers
|
||||
}
|
||||
|
||||
func (c Compiler) CompileVariables(optionalVars cel.OptionalVariableDeclarations) {
|
||||
c.compositedCompiler.CompileAndStoreVariables(
|
||||
c.convertVariables(),
|
||||
|
@ -45,7 +81,7 @@ func (c Compiler) CompileVariables(optionalVars cel.OptionalVariableDeclarations
|
|||
)
|
||||
}
|
||||
|
||||
func (c Compiler) CompileValidateExpressions(optionalVars cel.OptionalVariableDeclarations) cel.ConditionEvaluator {
|
||||
func (c Compiler) CompileValidations(optionalVars cel.OptionalVariableDeclarations) cel.ConditionEvaluator {
|
||||
return c.compositedCompiler.CompileCondition(
|
||||
c.convertValidations(),
|
||||
optionalVars,
|
||||
|
@ -69,17 +105,17 @@ func (c Compiler) CompileAuditAnnotationsExpressions(optionalVars cel.OptionalVa
|
|||
)
|
||||
}
|
||||
|
||||
func (c Compiler) CompileMatchExpressions(optionalVars cel.OptionalVariableDeclarations) cel.ConditionEvaluator {
|
||||
func (c Compiler) CompileMatchConditions(optionalVars cel.OptionalVariableDeclarations) cel.ConditionEvaluator {
|
||||
return c.compositedCompiler.CompileCondition(
|
||||
c.convertMatchExpressions(),
|
||||
c.convertMatchConditions(),
|
||||
optionalVars,
|
||||
environment.StoredExpressions,
|
||||
)
|
||||
}
|
||||
|
||||
func (c Compiler) convertValidations() []cel.ExpressionAccessor {
|
||||
celExpressionAccessor := make([]cel.ExpressionAccessor, len(c.validateExpressions))
|
||||
for i, validation := range c.validateExpressions {
|
||||
celExpressionAccessor := make([]cel.ExpressionAccessor, len(c.validations))
|
||||
for i, validation := range c.validations {
|
||||
validation := validating.ValidationCondition{
|
||||
Expression: validation.Expression,
|
||||
Message: validation.Message,
|
||||
|
@ -91,8 +127,8 @@ func (c Compiler) convertValidations() []cel.ExpressionAccessor {
|
|||
}
|
||||
|
||||
func (c Compiler) convertMessageExpressions() []cel.ExpressionAccessor {
|
||||
celExpressionAccessor := make([]cel.ExpressionAccessor, len(c.validateExpressions))
|
||||
for i, validation := range c.validateExpressions {
|
||||
celExpressionAccessor := make([]cel.ExpressionAccessor, len(c.validations))
|
||||
for i, validation := range c.validations {
|
||||
if validation.MessageExpression != "" {
|
||||
condition := validating.MessageExpressionCondition{
|
||||
MessageExpression: validation.MessageExpression,
|
||||
|
@ -104,8 +140,8 @@ func (c Compiler) convertMessageExpressions() []cel.ExpressionAccessor {
|
|||
}
|
||||
|
||||
func (c Compiler) convertAuditAnnotations() []cel.ExpressionAccessor {
|
||||
celExpressionAccessor := make([]cel.ExpressionAccessor, len(c.auditAnnotationExpressions))
|
||||
for i, validation := range c.auditAnnotationExpressions {
|
||||
celExpressionAccessor := make([]cel.ExpressionAccessor, len(c.auditAnnotations))
|
||||
for i, validation := range c.auditAnnotations {
|
||||
validation := validating.AuditAnnotationCondition{
|
||||
Key: validation.Key,
|
||||
ValueExpression: validation.ValueExpression,
|
||||
|
@ -115,9 +151,9 @@ func (c Compiler) convertAuditAnnotations() []cel.ExpressionAccessor {
|
|||
return celExpressionAccessor
|
||||
}
|
||||
|
||||
func (c Compiler) convertMatchExpressions() []cel.ExpressionAccessor {
|
||||
celExpressionAccessor := make([]cel.ExpressionAccessor, len(c.matchExpressions))
|
||||
for i, condition := range c.matchExpressions {
|
||||
func (c Compiler) convertMatchConditions() []cel.ExpressionAccessor {
|
||||
celExpressionAccessor := make([]cel.ExpressionAccessor, len(c.matchConditions))
|
||||
for i, condition := range c.matchConditions {
|
||||
condition := matchconditions.MatchCondition{
|
||||
Name: condition.Name,
|
||||
Expression: condition.Expression,
|
||||
|
|
135
pkg/admissionpolicy/mutate.go
Normal file
135
pkg/admissionpolicy/mutate.go
Normal file
|
@ -0,0 +1,135 @@
|
|||
package admissionpolicy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
celutils "github.com/kyverno/kyverno/pkg/cel/utils"
|
||||
engineapi "github.com/kyverno/kyverno/pkg/engine/api"
|
||||
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
|
||||
admissionregistrationv1alpha1 "k8s.io/api/admissionregistration/v1alpha1"
|
||||
admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/managedfields"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
|
||||
celconfig "k8s.io/apiserver/pkg/apis/cel"
|
||||
)
|
||||
|
||||
func mutateResource(
|
||||
policy admissionregistrationv1alpha1.MutatingAdmissionPolicy,
|
||||
resource unstructured.Unstructured,
|
||||
) (engineapi.EngineResponse, error) {
|
||||
startTime := time.Now()
|
||||
|
||||
engineResponse := engineapi.NewEngineResponse(resource, engineapi.NewMutatingAdmissionPolicy(policy), nil)
|
||||
policyResp := engineapi.NewPolicyResponse()
|
||||
|
||||
gvk := resource.GroupVersionKind()
|
||||
gvr := schema.GroupVersionResource{
|
||||
Group: gvk.Group,
|
||||
Version: gvk.Version,
|
||||
Resource: strings.ToLower(gvk.Kind) + "s",
|
||||
}
|
||||
|
||||
var namespace *corev1.Namespace
|
||||
namespaceName := resource.GetNamespace()
|
||||
// Special case, the namespace object has the namespace of itself.
|
||||
// unset it if the incoming object is a namespace
|
||||
if gvk.Kind == "Namespace" && gvk.Version == "v1" && gvk.Group == "" {
|
||||
namespaceName = ""
|
||||
}
|
||||
|
||||
if namespaceName != "" {
|
||||
namespace = &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: namespaceName,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
a := admission.NewAttributesRecord(resource.DeepCopyObject(), nil, resource.GroupVersionKind(), resource.GetNamespace(), resource.GetName(), gvr, "", admission.Create, nil, false, nil)
|
||||
versionedAttributes, _ := admission.NewVersionedAttributes(a, a.GetKind(), nil)
|
||||
o := admission.NewObjectInterfacesFromScheme(runtime.NewScheme())
|
||||
|
||||
matchConditions := make([]admissionregistrationv1.MatchCondition, 0, len(policy.Spec.MatchConditions))
|
||||
for _, m := range policy.Spec.MatchConditions {
|
||||
matchConditions = append(matchConditions, admissionregistrationv1.MatchCondition(m))
|
||||
}
|
||||
variables := make([]admissionregistrationv1beta1.Variable, 0, len(policy.Spec.Variables))
|
||||
for _, v := range policy.Spec.Variables {
|
||||
variables = append(variables, admissionregistrationv1beta1.Variable(v))
|
||||
}
|
||||
|
||||
// create compiler
|
||||
compiler, err := NewCompiler(matchConditions, variables)
|
||||
if err != nil {
|
||||
return engineResponse, err
|
||||
}
|
||||
compiler.WithMutations(policy.Spec.Mutations)
|
||||
|
||||
optionalVars := cel.OptionalVariableDeclarations{
|
||||
HasParams: false,
|
||||
HasAuthorizer: false,
|
||||
HasPatchTypes: true,
|
||||
StrictCost: true,
|
||||
}
|
||||
// compile variables
|
||||
compiler.CompileVariables(optionalVars)
|
||||
|
||||
var failPolicy admissionregistrationv1.FailurePolicyType
|
||||
if policy.Spec.FailurePolicy == nil {
|
||||
failPolicy = admissionregistrationv1.Fail
|
||||
} else {
|
||||
failPolicy = admissionregistrationv1.FailurePolicyType(*policy.Spec.FailurePolicy)
|
||||
}
|
||||
|
||||
// compile matchers
|
||||
matcher := matchconditions.NewMatcher(compiler.CompileMatchConditions(optionalVars), &failPolicy, "policy", "mutate", policy.Name)
|
||||
if matcher != nil {
|
||||
matchResults := matcher.Match(context.TODO(), versionedAttributes, nil, nil)
|
||||
|
||||
// if preconditions are not met, then skip mutations
|
||||
if !matchResults.Matches {
|
||||
return engineResponse, nil
|
||||
}
|
||||
}
|
||||
// compile mutations
|
||||
patchers := compiler.CompileMutations(optionalVars)
|
||||
// apply mutations
|
||||
for _, patcher := range patchers {
|
||||
patchRequest := patch.Request{
|
||||
MatchedResource: gvr,
|
||||
VersionedAttributes: versionedAttributes,
|
||||
ObjectInterfaces: o,
|
||||
OptionalVariables: cel.OptionalVariableBindings{VersionedParams: nil, Authorizer: nil},
|
||||
Namespace: namespace,
|
||||
TypeConverter: managedfields.NewDeducedTypeConverter(),
|
||||
}
|
||||
newVersionedObject, err := patcher.Patch(context.TODO(), patchRequest, celconfig.RuntimeCELCostBudget)
|
||||
if err != nil {
|
||||
return engineResponse, nil
|
||||
}
|
||||
versionedAttributes.Dirty = true
|
||||
versionedAttributes.VersionedObject = newVersionedObject
|
||||
}
|
||||
patchedResource, err := celutils.ConvertObjectToUnstructured(versionedAttributes.VersionedObject)
|
||||
if err != nil {
|
||||
return engineResponse, err
|
||||
}
|
||||
|
||||
ruleResp := engineapi.RulePass(policy.GetName(), engineapi.Mutation, "", nil)
|
||||
policyResp.Add(engineapi.NewExecutionStats(startTime, time.Now()), *ruleResp)
|
||||
engineResponse = engineResponse.
|
||||
WithPatchedResource(*patchedResource).
|
||||
WithPolicyResponse(policyResp)
|
||||
|
||||
return engineResponse, nil
|
||||
}
|
577
pkg/admissionpolicy/mutate_test.go
Normal file
577
pkg/admissionpolicy/mutate_test.go
Normal file
|
@ -0,0 +1,577 @@
|
|||
package admissionpolicy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
kubeutils "github.com/kyverno/kyverno/pkg/utils/kube"
|
||||
"gotest.tools/assert"
|
||||
admissionregistrationv1alpha1 "k8s.io/api/admissionregistration/v1alpha1"
|
||||
)
|
||||
|
||||
func Test_MutateResource(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
rawPolicy []byte
|
||||
rawResource []byte
|
||||
expectedRawResource []byte
|
||||
}{
|
||||
{
|
||||
name: "MAP ApplyConfiguration",
|
||||
rawPolicy: []byte(`{
|
||||
"apiVersion": "admissionregistration.k8s.io/v1alpha1",
|
||||
"kind": "MutatingAdmissionPolicy",
|
||||
"metadata": {
|
||||
"name": "mutate-policy"
|
||||
},
|
||||
"spec": {
|
||||
"matchConstraints": {
|
||||
"resourceRules": [
|
||||
{
|
||||
"apiGroups": [
|
||||
""
|
||||
],
|
||||
"apiVersions": [
|
||||
"v1"
|
||||
],
|
||||
"operations": [
|
||||
"CREATE"
|
||||
],
|
||||
"resources": [
|
||||
"configmaps"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"failurePolicy": "Fail",
|
||||
"mutations": [
|
||||
{
|
||||
"patchType": "ApplyConfiguration",
|
||||
"applyConfiguration": {
|
||||
"expression": "object.metadata.?labels[\"lfx-mentorship\"].hasValue() ? \n Object{} :\n Object{ metadata: Object.metadata{ labels: {\"lfx-mentorship\": \"kyverno\"}}}\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}`),
|
||||
rawResource: []byte(`{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": {
|
||||
"name": "game-demo",
|
||||
"labels": {
|
||||
"app": "game"
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"player_initial_lives": "3"
|
||||
}
|
||||
}`),
|
||||
expectedRawResource: []byte(`{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": {
|
||||
"name": "game-demo",
|
||||
"labels": {
|
||||
"app": "game",
|
||||
"lfx-mentorship": "kyverno"
|
||||
}
|
||||
},
|
||||
"data": {
|
||||
"player_initial_lives": "3"
|
||||
}
|
||||
}`),
|
||||
},
|
||||
{
|
||||
name: "MAP JSONPatch",
|
||||
rawPolicy: []byte(`{
|
||||
"apiVersion": "admissionregistration.k8s.io/v1alpha1",
|
||||
"kind": "MutatingAdmissionPolicy",
|
||||
"metadata": {
|
||||
"name": "mutate-policy"
|
||||
},
|
||||
"spec": {
|
||||
"matchConstraints": {
|
||||
"resourceRules": [
|
||||
{
|
||||
"apiGroups": [
|
||||
"discovery.k8s.io"
|
||||
],
|
||||
"apiVersions": [
|
||||
"v1"
|
||||
],
|
||||
"operations": [
|
||||
"CREATE"
|
||||
],
|
||||
"resources": [
|
||||
"endpointslices"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"failurePolicy": "Fail",
|
||||
"reinvocationPolicy": "Never",
|
||||
"mutations": [
|
||||
{
|
||||
"patchType": "JSONPatch",
|
||||
"jsonPatch": {
|
||||
"expression": "[\n JSONPatch{\n op: \"add\", path: \"/ports\",\n value: object.ports.map(\n p, \n {\n \"name\": p.name,\n \"port\": dyn(p.name.contains(\"secure\") ? 6443 : p.port)\n }\n )\n }\n]\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}`),
|
||||
rawResource: []byte(`{
|
||||
"apiVersion": "discovery.k8s.io/v1",
|
||||
"kind": "EndpointSlice",
|
||||
"metadata": {
|
||||
"name": "example-abc",
|
||||
"labels": {
|
||||
"kubernetes.io/service-name": "example"
|
||||
}
|
||||
},
|
||||
"addressType": "IPv4",
|
||||
"ports": [
|
||||
{
|
||||
"name": "http",
|
||||
"protocol": "TCP",
|
||||
"port": 80
|
||||
},
|
||||
{
|
||||
"name": "secure",
|
||||
"protocol": "TCP"
|
||||
}
|
||||
],
|
||||
"endpoints": [
|
||||
{
|
||||
"addresses": [
|
||||
"10.1.2.3"
|
||||
],
|
||||
"conditions": {
|
||||
"ready": true
|
||||
},
|
||||
"hostname": "pod-1",
|
||||
"nodeName": "node-1",
|
||||
"zone": "us-west2-a"
|
||||
}
|
||||
]
|
||||
}`),
|
||||
expectedRawResource: []byte(`{
|
||||
"apiVersion": "discovery.k8s.io/v1",
|
||||
"kind": "EndpointSlice",
|
||||
"metadata": {
|
||||
"name": "example-abc",
|
||||
"labels": {
|
||||
"kubernetes.io/service-name": "example"
|
||||
}
|
||||
},
|
||||
"addressType": "IPv4",
|
||||
"ports": [
|
||||
{
|
||||
"name": "http",
|
||||
"port": 80
|
||||
},
|
||||
{
|
||||
"name": "secure",
|
||||
"port": 6443
|
||||
}
|
||||
],
|
||||
"endpoints": [
|
||||
{
|
||||
"addresses": [
|
||||
"10.1.2.3"
|
||||
],
|
||||
"conditions": {
|
||||
"ready": true
|
||||
},
|
||||
"hostname": "pod-1",
|
||||
"nodeName": "node-1",
|
||||
"zone": "us-west2-a"
|
||||
}
|
||||
]
|
||||
}`),
|
||||
},
|
||||
{
|
||||
name: "MAP JSONPatch and ApplyConfiguration",
|
||||
rawPolicy: []byte(`{
|
||||
"apiVersion": "admissionregistration.k8s.io/v1alpha1",
|
||||
"kind": "MutatingAdmissionPolicy",
|
||||
"metadata": {
|
||||
"name": "sample-policy"
|
||||
},
|
||||
"spec": {
|
||||
"matchConstraints": {
|
||||
"resourceRules": [
|
||||
{
|
||||
"apiGroups": [
|
||||
"apps"
|
||||
],
|
||||
"apiVersions": [
|
||||
"v1"
|
||||
],
|
||||
"operations": [
|
||||
"CREATE"
|
||||
],
|
||||
"resources": [
|
||||
"deployments"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"failurePolicy": "Fail",
|
||||
"mutations": [
|
||||
{
|
||||
"patchType": "ApplyConfiguration",
|
||||
"applyConfiguration": {
|
||||
"expression": "Object{\n spec: Object.spec{\n replicas: object.spec.replicas + 100\n }\n}\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"patchType": "JSONPatch",
|
||||
"jsonPatch": {
|
||||
"expression": "[\n JSONPatch{\n op: \"replace\", \n path: \"/spec/replicas\", \n value: object.spec.replicas + 10\n }\n]\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}`),
|
||||
rawResource: []byte(`{
|
||||
"apiVersion": "apps/v1",
|
||||
"kind": "Deployment",
|
||||
"metadata": {
|
||||
"name": "nginx-deployment",
|
||||
"labels": {
|
||||
"app": "nginx"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"replicas": 3,
|
||||
"selector": {
|
||||
"matchLabels": {
|
||||
"app": "nginx"
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"metadata": {
|
||||
"labels": {
|
||||
"app": "nginx"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"containers": [
|
||||
{
|
||||
"name": "nginx",
|
||||
"image": "nginx:1.14.2",
|
||||
"ports": [
|
||||
{
|
||||
"containerPort": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}`),
|
||||
expectedRawResource: []byte(`{
|
||||
"apiVersion": "apps/v1",
|
||||
"kind": "Deployment",
|
||||
"metadata": {
|
||||
"name": "nginx-deployment",
|
||||
"labels": {
|
||||
"app": "nginx"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"replicas": 113,
|
||||
"selector": {
|
||||
"matchLabels": {
|
||||
"app": "nginx"
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"metadata": {
|
||||
"labels": {
|
||||
"app": "nginx"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"containers": [
|
||||
{
|
||||
"name": "nginx",
|
||||
"image": "nginx:1.14.2",
|
||||
"ports": [
|
||||
{
|
||||
"containerPort": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}`),
|
||||
},
|
||||
{
|
||||
name: "Two mutations of type ApplyConfigurations",
|
||||
rawPolicy: []byte(`{
|
||||
"apiVersion": "admissionregistration.k8s.io/v1alpha1",
|
||||
"kind": "MutatingAdmissionPolicy",
|
||||
"metadata": {
|
||||
"name": "sample-policy"
|
||||
},
|
||||
"spec": {
|
||||
"matchConstraints": {
|
||||
"resourceRules": [
|
||||
{
|
||||
"apiGroups": [
|
||||
"apps"
|
||||
],
|
||||
"apiVersions": [
|
||||
"v1"
|
||||
],
|
||||
"operations": [
|
||||
"CREATE"
|
||||
],
|
||||
"resources": [
|
||||
"deployments"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"failurePolicy": "Fail",
|
||||
"mutations": [
|
||||
{
|
||||
"patchType": "ApplyConfiguration",
|
||||
"applyConfiguration": {
|
||||
"expression": "Object{\n spec: Object.spec{\n replicas: object.spec.replicas + 100\n }\n}\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"patchType": "ApplyConfiguration",
|
||||
"applyConfiguration": {
|
||||
"expression": "Object{\n spec: Object.spec{\n replicas: object.spec.replicas + 100\n }\n}\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}`),
|
||||
rawResource: []byte(`{
|
||||
"apiVersion": "apps/v1",
|
||||
"kind": "Deployment",
|
||||
"metadata": {
|
||||
"name": "nginx-deployment",
|
||||
"labels": {
|
||||
"app": "nginx"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"replicas": 3,
|
||||
"selector": {
|
||||
"matchLabels": {
|
||||
"app": "nginx"
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"metadata": {
|
||||
"labels": {
|
||||
"app": "nginx"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"containers": [
|
||||
{
|
||||
"name": "nginx",
|
||||
"image": "nginx:1.14.2",
|
||||
"ports": [
|
||||
{
|
||||
"containerPort": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}`),
|
||||
expectedRawResource: []byte(`{
|
||||
"apiVersion": "apps/v1",
|
||||
"kind": "Deployment",
|
||||
"metadata": {
|
||||
"name": "nginx-deployment",
|
||||
"labels": {
|
||||
"app": "nginx"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"replicas": 203,
|
||||
"selector": {
|
||||
"matchLabels": {
|
||||
"app": "nginx"
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"metadata": {
|
||||
"labels": {
|
||||
"app": "nginx"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"containers": [
|
||||
{
|
||||
"name": "nginx",
|
||||
"image": "nginx:1.14.2",
|
||||
"ports": [
|
||||
{
|
||||
"containerPort": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}`),
|
||||
},
|
||||
{
|
||||
name: "Two mutations of type JSONPatch",
|
||||
rawPolicy: []byte(`{
|
||||
"apiVersion": "admissionregistration.k8s.io/v1alpha1",
|
||||
"kind": "MutatingAdmissionPolicy",
|
||||
"metadata": {
|
||||
"name": "sample-policy"
|
||||
},
|
||||
"spec": {
|
||||
"matchConstraints": {
|
||||
"resourceRules": [
|
||||
{
|
||||
"apiGroups": [
|
||||
"apps"
|
||||
],
|
||||
"apiVersions": [
|
||||
"v1"
|
||||
],
|
||||
"operations": [
|
||||
"CREATE"
|
||||
],
|
||||
"resources": [
|
||||
"deployments"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"failurePolicy": "Fail",
|
||||
"mutations": [
|
||||
{
|
||||
"patchType": "JSONPatch",
|
||||
"jsonPatch": {
|
||||
"expression": "[\n JSONPatch{\n op: \"replace\", \n path: \"/spec/replicas\", \n value: object.spec.replicas + 10\n }\n]\n"
|
||||
}
|
||||
},
|
||||
{
|
||||
"patchType": "JSONPatch",
|
||||
"jsonPatch": {
|
||||
"expression": "[\n JSONPatch{\n op: \"replace\", \n path: \"/spec/replicas\", \n value: object.spec.replicas + 10\n }\n]\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}`),
|
||||
rawResource: []byte(`{
|
||||
"apiVersion": "apps/v1",
|
||||
"kind": "Deployment",
|
||||
"metadata": {
|
||||
"name": "nginx-deployment",
|
||||
"labels": {
|
||||
"app": "nginx"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"replicas": 3,
|
||||
"selector": {
|
||||
"matchLabels": {
|
||||
"app": "nginx"
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"metadata": {
|
||||
"labels": {
|
||||
"app": "nginx"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"containers": [
|
||||
{
|
||||
"name": "nginx",
|
||||
"image": "nginx:1.14.2",
|
||||
"ports": [
|
||||
{
|
||||
"containerPort": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}`),
|
||||
expectedRawResource: []byte(`{
|
||||
"apiVersion": "apps/v1",
|
||||
"kind": "Deployment",
|
||||
"metadata": {
|
||||
"name": "nginx-deployment",
|
||||
"labels": {
|
||||
"app": "nginx"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"replicas": 23,
|
||||
"selector": {
|
||||
"matchLabels": {
|
||||
"app": "nginx"
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"metadata": {
|
||||
"labels": {
|
||||
"app": "nginx"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"containers": [
|
||||
{
|
||||
"name": "nginx",
|
||||
"image": "nginx:1.14.2",
|
||||
"ports": [
|
||||
{
|
||||
"containerPort": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}`),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
expectedResource, err := kubeutils.BytesToUnstructured(tt.expectedRawResource)
|
||||
assert.NilError(t, err)
|
||||
|
||||
var policy admissionregistrationv1alpha1.MutatingAdmissionPolicy
|
||||
err = json.Unmarshal(tt.rawPolicy, &policy)
|
||||
assert.NilError(t, err)
|
||||
|
||||
resource, err := kubeutils.BytesToUnstructured(tt.rawResource)
|
||||
assert.NilError(t, err)
|
||||
|
||||
response, err := mutateResource(policy, *resource)
|
||||
assert.NilError(t, err)
|
||||
|
||||
assert.DeepEqual(t, expectedResource.Object, response.PatchedResource.Object)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -192,10 +192,13 @@ func validateResource(
|
|||
|
||||
// compile CEL expressions
|
||||
matchConditions := ConvertMatchConditionsV1(policy.Spec.MatchConditions)
|
||||
compiler, err := NewCompiler(policy.Spec.Validations, policy.Spec.AuditAnnotations, matchConditions, policy.Spec.Variables)
|
||||
compiler, err := NewCompiler(matchConditions, policy.Spec.Variables)
|
||||
if err != nil {
|
||||
return engineResponse, err
|
||||
}
|
||||
compiler.WithValidations(policy.Spec.Validations)
|
||||
compiler.WithAuditAnnotations(policy.Spec.AuditAnnotations)
|
||||
|
||||
hasParam := policy.Spec.ParamKind != nil
|
||||
optionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: false}
|
||||
compiler.CompileVariables(optionalVars)
|
||||
|
@ -214,9 +217,9 @@ func validateResource(
|
|||
matchPolicy = *policy.Spec.MatchConstraints.MatchPolicy
|
||||
}
|
||||
|
||||
newMatcher := matchconditions.NewMatcher(compiler.CompileMatchExpressions(optionalVars), &failPolicy, "", string(matchPolicy), "")
|
||||
newMatcher := matchconditions.NewMatcher(compiler.CompileMatchConditions(optionalVars), &failPolicy, "", string(matchPolicy), "")
|
||||
validator := validating.NewValidator(
|
||||
compiler.CompileValidateExpressions(optionalVars),
|
||||
compiler.CompileValidations(optionalVars),
|
||||
newMatcher,
|
||||
compiler.CompileAuditAnnotationsExpressions(optionalVars),
|
||||
compiler.CompileMessageExpressions(optionalVars),
|
||||
|
|
|
@ -2,6 +2,7 @@ package api
|
|||
|
||||
import (
|
||||
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
|
||||
admissionregistrationv1alpha1 "k8s.io/api/admissionregistration/v1alpha1"
|
||||
admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
@ -12,8 +13,10 @@ type PolicyType string
|
|||
const (
|
||||
// KyvernoPolicy type for kyverno policies
|
||||
KyvernoPolicyType PolicyType = "KyvernoPolicy"
|
||||
// ValidatingAdmissionPolicy for validating admission policies
|
||||
// ValidatingAdmissionPolicy for Kubernetes ValidatingAdmission policies
|
||||
ValidatingAdmissionPolicyType PolicyType = "ValidatingAdmissionPolicy"
|
||||
// MutatingAdmissionPolicy for Kubernetes MutatingAdmissionPolicies
|
||||
MutatingAdmissionPolicyType PolicyType = "MutatingAdmissionPolicy"
|
||||
)
|
||||
|
||||
// GenericPolicy abstracts the policy type (Kyverno policy vs Validating admission policy)
|
||||
|
@ -150,3 +153,57 @@ func NewValidatingAdmissionPolicy(pol admissionregistrationv1beta1.ValidatingAdm
|
|||
policy: pol,
|
||||
}
|
||||
}
|
||||
|
||||
type MutatingAdmissionPolicy struct {
|
||||
policy admissionregistrationv1alpha1.MutatingAdmissionPolicy
|
||||
}
|
||||
|
||||
func (p *MutatingAdmissionPolicy) AsKyvernoPolicy() kyvernov1.PolicyInterface {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *MutatingAdmissionPolicy) AsValidatingAdmissionPolicy() *admissionregistrationv1beta1.ValidatingAdmissionPolicy {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *MutatingAdmissionPolicy) GetType() PolicyType {
|
||||
return MutatingAdmissionPolicyType
|
||||
}
|
||||
|
||||
func (p *MutatingAdmissionPolicy) GetAPIVersion() string {
|
||||
return "admissionregistration.k8s.io/v1alpha1"
|
||||
}
|
||||
|
||||
func (p *MutatingAdmissionPolicy) GetName() string {
|
||||
return p.policy.GetName()
|
||||
}
|
||||
|
||||
func (p *MutatingAdmissionPolicy) GetNamespace() string {
|
||||
return p.policy.GetNamespace()
|
||||
}
|
||||
|
||||
func (p *MutatingAdmissionPolicy) GetKind() string {
|
||||
return "MutatingAdmissionPolicy"
|
||||
}
|
||||
|
||||
func (p *MutatingAdmissionPolicy) GetResourceVersion() string {
|
||||
return p.policy.GetResourceVersion()
|
||||
}
|
||||
|
||||
func (p *MutatingAdmissionPolicy) GetAnnotations() map[string]string {
|
||||
return p.policy.GetAnnotations()
|
||||
}
|
||||
|
||||
func (p *MutatingAdmissionPolicy) IsNamespaced() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *MutatingAdmissionPolicy) MetaObject() metav1.Object {
|
||||
return &p.policy
|
||||
}
|
||||
|
||||
func NewMutatingAdmissionPolicy(pol admissionregistrationv1alpha1.MutatingAdmissionPolicy) GenericPolicy {
|
||||
return &MutatingAdmissionPolicy{
|
||||
policy: pol,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -119,15 +119,17 @@ func (h validateCELHandler) Process(
|
|||
optionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: true}
|
||||
expressionOptionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: false}
|
||||
// compile CEL expressions
|
||||
compiler, err := admissionpolicy.NewCompiler(validations, auditAnnotations, admissionpolicy.ConvertMatchConditionsV1(matchConditions), variables)
|
||||
compiler, err := admissionpolicy.NewCompiler(admissionpolicy.ConvertMatchConditionsV1(matchConditions), variables)
|
||||
if err != nil {
|
||||
return resource, handlers.WithError(rule, engineapi.Validation, "Error while creating composited compiler", err)
|
||||
}
|
||||
compiler.WithValidations(validations)
|
||||
compiler.WithAuditAnnotations(auditAnnotations)
|
||||
compiler.CompileVariables(optionalVars)
|
||||
filter := compiler.CompileValidateExpressions(optionalVars)
|
||||
filter := compiler.CompileValidations(optionalVars)
|
||||
messageExpressionfilter := compiler.CompileMessageExpressions(expressionOptionalVars)
|
||||
auditAnnotationFilter := compiler.CompileAuditAnnotationsExpressions(optionalVars)
|
||||
matchConditionFilter := compiler.CompileMatchExpressions(optionalVars)
|
||||
matchConditionFilter := compiler.CompileMatchConditions(optionalVars)
|
||||
|
||||
// newMatcher will be used to check if the incoming resource matches the CEL preconditions
|
||||
newMatcher := matchconditions.NewMatcher(matchConditionFilter, nil, policyKind, "", policyName)
|
||||
|
|
|
@ -88,12 +88,12 @@ func checkMatchConditions(logger logr.Logger, policyContext engineapi.PolicyCont
|
|||
}
|
||||
|
||||
optionalVars := cel.OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false}
|
||||
compiler, err := admissionpolicy.NewCompiler(nil, nil, policy.GetSpec().GetMatchConditions(), nil)
|
||||
compiler, err := admissionpolicy.NewCompiler(policy.GetSpec().GetMatchConditions(), nil)
|
||||
if err != nil {
|
||||
logger.Error(err, "error creating composited compiler")
|
||||
return false
|
||||
}
|
||||
matchConditionFilter := compiler.CompileMatchExpressions(optionalVars)
|
||||
matchConditionFilter := compiler.CompileMatchConditions(optionalVars)
|
||||
matcher := matchconditions.NewMatcher(matchConditionFilter, nil, policy.GetKind(), "", policy.GetName())
|
||||
result := matcher.Match(context.TODO(), versionedAttr, nil, nil)
|
||||
return result.Matches
|
||||
|
|
|
@ -541,11 +541,11 @@ func isGlobalContextEntryReady(name string, gctxentries *kyvernov2alpha1.GlobalC
|
|||
}
|
||||
|
||||
func ValidateCustomWebhookMatchConditions(wc []admissionregistrationv1.MatchCondition) error {
|
||||
c, err := admissionpolicy.NewCompiler(nil, nil, wc, nil)
|
||||
c, err := admissionpolicy.NewCompiler(wc, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f := c.CompileMatchExpressions(cel.OptionalVariableDeclarations{})
|
||||
f := c.CompileMatchConditions(cel.OptionalVariableDeclarations{})
|
||||
if len(f.CompilationErrors()) > 0 {
|
||||
return fmt.Errorf("match conditions compilation errors: %v", f.CompilationErrors())
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue