1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-31 03:45:17 +00:00

fix: deferred loader panic when mutate and generate policies are applied (#9935)

* fix: deferred loader panic when mutate and generate policies are applied

Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com>

* fix: tests

Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com>

* fix: update policies

Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com>

* remove clusterrolebinding

Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com>

* fix: copy only json context

Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com>

* fix: polctx

Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com>

---------

Signed-off-by: Vishal Choudhary <vishal.choudhary@nirmata.com>
Co-authored-by: shuting <shuting@nirmata.com>
This commit is contained in:
Vishal Choudhary 2024-03-29 19:07:15 +05:30 committed by GitHub
parent 1a1954002f
commit 93eac3f7a4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 621 additions and 144 deletions

View file

@ -10,7 +10,7 @@ import (
func TestDeferredLoaderMatch(t *testing.T) { func TestDeferredLoaderMatch(t *testing.T) {
ctx := newContext() ctx := newContext()
mockLoader, _ := addDeferred(ctx, "one", "1") mockLoader, _ := AddMockDeferredLoader(ctx, "one", "1")
assert.Equal(t, 0, mockLoader.invocations) assert.Equal(t, 0, mockLoader.invocations)
val, err := ctx.Query("one") val, err := ctx.Query("one")
@ -22,28 +22,28 @@ func TestDeferredLoaderMatch(t *testing.T) {
assert.Equal(t, 1, mockLoader.invocations) assert.Equal(t, 1, mockLoader.invocations)
ctx = newContext() ctx = newContext()
ml, _ := addDeferred(ctx, "one", "1") ml, _ := AddMockDeferredLoader(ctx, "one", "1")
testCheckMatch(t, ctx, "one<two", "one", "1", ml) testCheckMatch(t, ctx, "one<two", "one", "1", ml)
ml, _ = addDeferred(ctx, "one", "1") ml, _ = AddMockDeferredLoader(ctx, "one", "1")
testCheckMatch(t, ctx, "(one)", "one", "1", ml) testCheckMatch(t, ctx, "(one)", "one", "1", ml)
ml, _ = addDeferred(ctx, "one", "1") ml, _ = AddMockDeferredLoader(ctx, "one", "1")
testCheckMatch(t, ctx, "one.two.three", "one", "1", ml) testCheckMatch(t, ctx, "one.two.three", "one", "1", ml)
ml, _ = addDeferred(ctx, "one", "1") ml, _ = AddMockDeferredLoader(ctx, "one", "1")
testCheckMatch(t, ctx, "one-two", "one", "1", ml) testCheckMatch(t, ctx, "one-two", "one", "1", ml)
ml, _ = addDeferred(ctx, "one", "1") ml, _ = AddMockDeferredLoader(ctx, "one", "1")
testCheckMatch(t, ctx, "one; two; three", "one", "1", ml) testCheckMatch(t, ctx, "one; two; three", "one", "1", ml)
ml, _ = addDeferred(ctx, "one", "1") ml, _ = AddMockDeferredLoader(ctx, "one", "1")
testCheckMatch(t, ctx, "one>two", "one", "1", ml) testCheckMatch(t, ctx, "one>two", "one", "1", ml)
ml, _ = addDeferred(ctx, "one", "1") ml, _ = AddMockDeferredLoader(ctx, "one", "1")
testCheckMatch(t, ctx, "one, two, three", "one", "1", ml) testCheckMatch(t, ctx, "one, two, three", "one", "1", ml)
ml, _ = addDeferred(ctx, "one1", "11") ml, _ = AddMockDeferredLoader(ctx, "one1", "11")
testCheckMatch(t, ctx, "one1", "one1", "11", ml) testCheckMatch(t, ctx, "one1", "one1", "11", ml)
} }
@ -64,7 +64,7 @@ func testCheckMatch(t *testing.T, ctx *context, query, name, value string, ml *m
func TestDeferredLoaderMismatch(t *testing.T) { func TestDeferredLoaderMismatch(t *testing.T) {
ctx := newContext() ctx := newContext()
addDeferred(ctx, "one", "1") AddMockDeferredLoader(ctx, "one", "1")
_, err := ctx.Query("oneTwoThree") _, err := ctx.Query("oneTwoThree")
assert.ErrorContains(t, err, `Unknown key "oneTwoThree" in path`) assert.ErrorContains(t, err, `Unknown key "oneTwoThree" in path`)
@ -101,92 +101,9 @@ func newContext() *context {
} }
} }
type mockLoader struct {
name string
level int
value interface{}
query string
hasLoaded bool
invocations int
eventHandler func(event string)
ctx *context
}
func (ml *mockLoader) Name() string {
return ml.name
}
func (ml *mockLoader) SetLevel(level int) {
ml.level = level
}
func (ml *mockLoader) GetLevel() int {
return ml.level
}
func (ml *mockLoader) HasLoaded() bool {
return ml.hasLoaded
}
func (ml *mockLoader) LoadData() error {
ml.invocations++
ml.ctx.AddVariable(ml.name, ml.value)
// simulate a JMESPath evaluation after loading
if err := ml.executeQuery(); err != nil {
return err
}
ml.hasLoaded = true
if ml.eventHandler != nil {
event := fmt.Sprintf("%s=%v", ml.name, ml.value)
ml.eventHandler(event)
}
return nil
}
func (ml *mockLoader) executeQuery() error {
if ml.query == "" {
return nil
}
results, err := ml.ctx.Query(ml.query)
if err != nil {
return err
}
return ml.ctx.AddVariable(ml.name, results)
}
func (ml *mockLoader) setEventHandler(eventHandler func(string)) {
ml.eventHandler = eventHandler
}
func addDeferred(ctx *context, name string, value interface{}) (*mockLoader, error) {
return addDeferredWithQuery(ctx, name, value, "")
}
func addDeferredWithQuery(ctx *context, name string, value interface{}, query string) (*mockLoader, error) {
loader := &mockLoader{
name: name,
value: value,
ctx: ctx,
query: query,
}
d, err := NewDeferredLoader(name, loader, logger)
if err != nil {
return loader, err
}
ctx.AddDeferredLoader(d)
return loader, nil
}
func TestDeferredReset(t *testing.T) { func TestDeferredReset(t *testing.T) {
ctx := newContext() ctx := newContext()
addDeferred(ctx, "value", "0") AddMockDeferredLoader(ctx, "value", "0")
ctx.Checkpoint() ctx.Checkpoint()
val, err := ctx.Query("value") val, err := ctx.Query("value")
@ -203,8 +120,8 @@ func TestDeferredCheckpointRestore(t *testing.T) {
ctx := newContext() ctx := newContext()
ctx.Checkpoint() ctx.Checkpoint()
unused, _ := addDeferred(ctx, "unused", "unused") unused, _ := AddMockDeferredLoader(ctx, "unused", "unused")
mock, _ := addDeferred(ctx, "one", "1") mock, _ := AddMockDeferredLoader(ctx, "one", "1")
ctx.Restore() ctx.Restore()
assert.Equal(t, 0, mock.invocations) assert.Equal(t, 0, mock.invocations)
assert.Equal(t, 0, unused.invocations) assert.Equal(t, 0, unused.invocations)
@ -219,7 +136,7 @@ func TestDeferredCheckpointRestore(t *testing.T) {
_, err = ctx.Query("one") _, err = ctx.Query("one")
assert.ErrorContains(t, err, "Unknown key \"one\" in path") assert.ErrorContains(t, err, "Unknown key \"one\" in path")
_, _ = addDeferred(ctx, "one", "1") _, _ = AddMockDeferredLoader(ctx, "one", "1")
ctx.Checkpoint() ctx.Checkpoint()
one, err := ctx.Query("one") one, err := ctx.Query("one")
assert.NilError(t, err) assert.NilError(t, err)
@ -235,14 +152,14 @@ func TestDeferredCheckpointRestore(t *testing.T) {
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, "1", one) assert.Equal(t, "1", one)
mock, _ = addDeferred(ctx, "one", "1") mock, _ = AddMockDeferredLoader(ctx, "one", "1")
ctx.Checkpoint() ctx.Checkpoint()
val, err := ctx.Query("one") val, err := ctx.Query("one")
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, "1", val) assert.Equal(t, "1", val)
assert.Equal(t, 1, mock.invocations) assert.Equal(t, 1, mock.invocations)
mock2, _ := addDeferred(ctx, "two", "2") mock2, _ := AddMockDeferredLoader(ctx, "two", "2")
val, err = ctx.Query("two") val, err = ctx.Query("two")
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, "2", val) assert.Equal(t, "2", val)
@ -269,7 +186,7 @@ func TestDeferredCheckpointRestore(t *testing.T) {
_, err = ctx.Query("two") _, err = ctx.Query("two")
assert.ErrorContains(t, err, `Unknown key "two" in path`) assert.ErrorContains(t, err, `Unknown key "two" in path`)
mock3, _ := addDeferred(ctx, "three", "3") mock3, _ := AddMockDeferredLoader(ctx, "three", "3")
val, err = ctx.Query("three") val, err = ctx.Query("three")
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, "3", val) assert.Equal(t, "3", val)
@ -290,7 +207,7 @@ func TestDeferredCheckpointRestore(t *testing.T) {
func TestDeferredForloop(t *testing.T) { func TestDeferredForloop(t *testing.T) {
ctx := newContext() ctx := newContext()
addDeferred(ctx, "value", float64(-1)) AddMockDeferredLoader(ctx, "value", float64(-1))
ctx.Checkpoint() ctx.Checkpoint()
for i := 0; i < 5; i++ { for i := 0; i < 5; i++ {
@ -299,7 +216,7 @@ func TestDeferredForloop(t *testing.T) {
assert.Equal(t, float64(i-1), val) assert.Equal(t, float64(i-1), val)
ctx.Reset() ctx.Reset()
mock, _ := addDeferred(ctx, "value", float64(i)) mock, _ := AddMockDeferredLoader(ctx, "value", float64(i))
val, err = ctx.Query("value") val, err = ctx.Query("value")
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, float64(i), val) assert.Equal(t, float64(i), val)
@ -315,13 +232,13 @@ func TestDeferredForloop(t *testing.T) {
func TestDeferredInvalidReset(t *testing.T) { func TestDeferredInvalidReset(t *testing.T) {
ctx := newContext() ctx := newContext()
addDeferred(ctx, "value", "0") AddMockDeferredLoader(ctx, "value", "0")
ctx.Reset() // no checkpoint ctx.Reset() // no checkpoint
val, err := ctx.Query("value") val, err := ctx.Query("value")
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, "0", val) assert.Equal(t, "0", val)
addDeferred(ctx, "value", "0") AddMockDeferredLoader(ctx, "value", "0")
ctx.Restore() // no checkpoint ctx.Restore() // no checkpoint
val, err = ctx.Query("value") val, err = ctx.Query("value")
assert.NilError(t, err) assert.NilError(t, err)
@ -330,18 +247,18 @@ func TestDeferredInvalidReset(t *testing.T) {
func TestDeferredValidResetRestore(t *testing.T) { func TestDeferredValidResetRestore(t *testing.T) {
ctx := newContext() ctx := newContext()
addDeferred(ctx, "value", "0") AddMockDeferredLoader(ctx, "value", "0")
ctx.Checkpoint() ctx.Checkpoint()
addDeferred(ctx, "leak", "leak") AddMockDeferredLoader(ctx, "leak", "leak")
ctx.Reset() ctx.Reset()
_, err := ctx.Query("leak") _, err := ctx.Query("leak")
assert.ErrorContains(t, err, `Unknown key "leak" in path`) assert.ErrorContains(t, err, `Unknown key "leak" in path`)
addDeferred(ctx, "value", "0") AddMockDeferredLoader(ctx, "value", "0")
ctx.Checkpoint() ctx.Checkpoint()
addDeferred(ctx, "leak", "leak") AddMockDeferredLoader(ctx, "leak", "leak")
ctx.Restore() ctx.Restore()
_, err = ctx.Query("leak") _, err = ctx.Query("leak")
@ -355,10 +272,10 @@ func TestDeferredSameName(t *testing.T) {
sequence = append(sequence, name) sequence = append(sequence, name)
} }
mock1, _ := addDeferred(ctx, "value", "0") mock1, _ := AddMockDeferredLoader(ctx, "value", "0")
mock1.setEventHandler(hdlr) mock1.setEventHandler(hdlr)
mock2, _ := addDeferred(ctx, "value", "1") mock2, _ := AddMockDeferredLoader(ctx, "value", "1")
mock2.setEventHandler(hdlr) mock2.setEventHandler(hdlr)
val, err := ctx.Query("value") val, err := ctx.Query("value")
@ -383,7 +300,7 @@ func TestDeferredRecursive(t *testing.T) {
func TestJMESPathDependency(t *testing.T) { func TestJMESPathDependency(t *testing.T) {
ctx := newContext() ctx := newContext()
addDeferred(ctx, "foo", "foo") AddMockDeferredLoader(ctx, "foo", "foo")
addDeferredWithQuery(ctx, "one", "1", "foo") addDeferredWithQuery(ctx, "one", "1", "foo")
val, err := ctx.Query("one") val, err := ctx.Query("one")
@ -393,10 +310,10 @@ func TestJMESPathDependency(t *testing.T) {
func TestDeferredHiddenEval(t *testing.T) { func TestDeferredHiddenEval(t *testing.T) {
ctx := newContext() ctx := newContext()
addDeferred(ctx, "foo", "foo") AddMockDeferredLoader(ctx, "foo", "foo")
ctx.Checkpoint() ctx.Checkpoint()
addDeferred(ctx, "foo", "bar") AddMockDeferredLoader(ctx, "foo", "bar")
val, err := ctx.Query("foo") val, err := ctx.Query("foo")
assert.NilError(t, err) assert.NilError(t, err)
@ -405,11 +322,11 @@ func TestDeferredHiddenEval(t *testing.T) {
func TestDeferredNotHidden(t *testing.T) { func TestDeferredNotHidden(t *testing.T) {
ctx := newContext() ctx := newContext()
addDeferred(ctx, "foo", "foo") AddMockDeferredLoader(ctx, "foo", "foo")
addDeferredWithQuery(ctx, "one", "1", "foo") addDeferredWithQuery(ctx, "one", "1", "foo")
ctx.Checkpoint() ctx.Checkpoint()
addDeferred(ctx, "foo", "bar") AddMockDeferredLoader(ctx, "foo", "bar")
val, err := ctx.Query("one") val, err := ctx.Query("one")
assert.NilError(t, err) assert.NilError(t, err)
@ -418,12 +335,12 @@ func TestDeferredNotHidden(t *testing.T) {
func TestDeferredNotHiddenOrdered(t *testing.T) { func TestDeferredNotHiddenOrdered(t *testing.T) {
ctx := newContext() ctx := newContext()
addDeferred(ctx, "foo", "foo") AddMockDeferredLoader(ctx, "foo", "foo")
addDeferredWithQuery(ctx, "one", "1", "foo") addDeferredWithQuery(ctx, "one", "1", "foo")
addDeferred(ctx, "foo", "baz") AddMockDeferredLoader(ctx, "foo", "baz")
ctx.Checkpoint() ctx.Checkpoint()
addDeferred(ctx, "foo", "bar") AddMockDeferredLoader(ctx, "foo", "bar")
val, err := ctx.Query("one") val, err := ctx.Query("one")
assert.NilError(t, err) assert.NilError(t, err)
assert.Equal(t, "foo", val) assert.Equal(t, "foo", val)

View file

@ -0,0 +1,92 @@
package context
import "fmt"
type mockLoader struct {
name string
level int
value interface{}
query string
hasLoaded bool
invocations int
eventHandler func(event string)
ctx Interface
}
func (ml *mockLoader) Name() string {
return ml.name
}
func (ml *mockLoader) SetLevel(level int) {
ml.level = level
}
func (ml *mockLoader) GetLevel() int {
return ml.level
}
func (ml *mockLoader) HasLoaded() bool {
return ml.hasLoaded
}
func (ml *mockLoader) LoadData() error {
ml.invocations++
err := ml.ctx.AddVariable(ml.name, ml.value)
if err != nil {
return err
}
// simulate a JMESPath evaluation after loading
if err := ml.executeQuery(); err != nil {
return err
}
ml.hasLoaded = true
if ml.eventHandler != nil {
event := fmt.Sprintf("%s=%v", ml.name, ml.value)
ml.eventHandler(event)
}
return nil
}
func (ml *mockLoader) executeQuery() error {
if ml.query == "" {
return nil
}
results, err := ml.ctx.Query(ml.query)
if err != nil {
return err
}
return ml.ctx.AddVariable(ml.name, results)
}
func (ml *mockLoader) setEventHandler(eventHandler func(string)) {
ml.eventHandler = eventHandler
}
func AddMockDeferredLoader(ctx Interface, name string, value interface{}) (*mockLoader, error) {
return addDeferredWithQuery(ctx, name, value, "")
}
func addDeferredWithQuery(ctx Interface, name string, value interface{}, query string) (*mockLoader, error) {
loader := &mockLoader{
name: name,
value: value,
ctx: ctx,
query: query,
}
d, err := NewDeferredLoader(name, loader, logger)
if err != nil {
return loader, err
}
err = ctx.AddDeferredLoader(d)
if err != nil {
return nil, err
}
return loader, nil
}

View file

@ -18,14 +18,13 @@ import (
"github.com/kyverno/kyverno/pkg/metrics" "github.com/kyverno/kyverno/pkg/metrics"
"github.com/kyverno/kyverno/pkg/policycache" "github.com/kyverno/kyverno/pkg/policycache"
"github.com/kyverno/kyverno/pkg/registryclient" "github.com/kyverno/kyverno/pkg/registryclient"
"github.com/kyverno/kyverno/pkg/webhooks"
"github.com/kyverno/kyverno/pkg/webhooks/updaterequest" "github.com/kyverno/kyverno/pkg/webhooks/updaterequest"
webhookutils "github.com/kyverno/kyverno/pkg/webhooks/utils" webhookutils "github.com/kyverno/kyverno/pkg/webhooks/utils"
kubeinformers "k8s.io/client-go/informers" kubeinformers "k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/kubernetes/fake"
) )
func NewFakeHandlers(ctx context.Context, policyCache policycache.Cache) webhooks.ResourceHandlers { func NewFakeHandlers(ctx context.Context, policyCache policycache.Cache) *resourceHandlers {
client := fake.NewSimpleClientset() client := fake.NewSimpleClientset()
metricsConfig := metrics.NewFakeMetricsConfig() metricsConfig := metrics.NewFakeMetricsConfig()

View file

@ -5,6 +5,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"strings" "strings"
"sync"
"time" "time"
"github.com/go-logr/logr" "github.com/go-logr/logr"
@ -17,6 +18,7 @@ import (
"github.com/kyverno/kyverno/pkg/config" "github.com/kyverno/kyverno/pkg/config"
engineapi "github.com/kyverno/kyverno/pkg/engine/api" engineapi "github.com/kyverno/kyverno/pkg/engine/api"
"github.com/kyverno/kyverno/pkg/engine/jmespath" "github.com/kyverno/kyverno/pkg/engine/jmespath"
"github.com/kyverno/kyverno/pkg/engine/policycontext"
"github.com/kyverno/kyverno/pkg/event" "github.com/kyverno/kyverno/pkg/event"
"github.com/kyverno/kyverno/pkg/metrics" "github.com/kyverno/kyverno/pkg/metrics"
"github.com/kyverno/kyverno/pkg/policycache" "github.com/kyverno/kyverno/pkg/policycache"
@ -35,6 +37,8 @@ import (
) )
type resourceHandlers struct { type resourceHandlers struct {
wg sync.WaitGroup
// clients // clients
client dclient.Interface client dclient.Interface
kyvernoClient versioned.Interface kyvernoClient versioned.Interface
@ -113,16 +117,11 @@ func (h *resourceHandlers) Validate(ctx context.Context, logger logr.Logger, req
logger.V(4).Info("processing policies for validate admission request", "validate", len(policies), "mutate", len(mutatePolicies), "generate", len(generatePolicies)) logger.V(4).Info("processing policies for validate admission request", "validate", len(policies), "mutate", len(mutatePolicies), "generate", len(generatePolicies))
policyContext, err := h.pcBuilder.Build(request.AdmissionRequest, request.Roles, request.ClusterRoles, request.GroupVersionKind) policyContext, err := h.buildPolicyContextFromAdmissionRequest(logger, request)
if err != nil { if err != nil {
return errorResponse(logger, request.UID, err, "failed create policy context") return errorResponse(logger, request.UID, err, "failed create policy context")
} }
namespaceLabels := make(map[string]string)
if request.Kind.Kind != "Namespace" && request.Namespace != "" {
namespaceLabels = engineutils.GetNamespaceSelectorsFromNamespaceLister(request.Kind.Kind, request.Namespace, h.nsLister, logger)
}
policyContext = policyContext.WithNamespaceLabels(namespaceLabels)
vh := validation.NewValidationHandler(logger, h.kyvernoClient, h.engine, h.pCache, h.pcBuilder, h.eventGen, h.admissionReports, h.metricsConfig, h.configuration) vh := validation.NewValidationHandler(logger, h.kyvernoClient, h.engine, h.pCache, h.pcBuilder, h.eventGen, h.admissionReports, h.metricsConfig, h.configuration)
ok, msg, warnings := vh.HandleValidation(ctx, request, policies, policyContext, startTime) ok, msg, warnings := vh.HandleValidation(ctx, request, policies, policyContext, startTime)
@ -131,7 +130,9 @@ func (h *resourceHandlers) Validate(ctx context.Context, logger logr.Logger, req
return admissionutils.Response(request.UID, errors.New(msg), warnings...) return admissionutils.Response(request.UID, errors.New(msg), warnings...)
} }
if !admissionutils.IsDryRun(request.AdmissionRequest) { if !admissionutils.IsDryRun(request.AdmissionRequest) {
go h.handleBackgroundApplies(ctx, logger, request.AdmissionRequest, policyContext, generatePolicies, mutatePolicies, startTime) h.wg.Add(1)
go h.handleBackgroundApplies(ctx, logger, request, generatePolicies, mutatePolicies, startTime)
h.wg.Wait()
} }
return admissionutils.ResponseSuccess(request.UID, warnings...) return admissionutils.ResponseSuccess(request.UID, warnings...)
} }
@ -241,6 +242,19 @@ func (h *resourceHandlers) retrieveAndCategorizePolicies(
return policies, mutatePolicies, generatePolicies, imageVerifyValidatePolicies, nil return policies, mutatePolicies, generatePolicies, imageVerifyValidatePolicies, nil
} }
func (h *resourceHandlers) buildPolicyContextFromAdmissionRequest(logger logr.Logger, request handlers.AdmissionRequest) (*policycontext.PolicyContext, error) {
policyContext, err := h.pcBuilder.Build(request.AdmissionRequest, request.Roles, request.ClusterRoles, request.GroupVersionKind)
if err != nil {
return nil, err
}
namespaceLabels := make(map[string]string)
if request.Kind.Kind != "Namespace" && request.Namespace != "" {
namespaceLabels = engineutils.GetNamespaceSelectorsFromNamespaceLister(request.Kind.Kind, request.Namespace, h.nsLister, logger)
}
policyContext = policyContext.WithNamespaceLabels(namespaceLabels)
return policyContext, nil
}
func filterPolicies(ctx context.Context, failurePolicy string, policies ...kyvernov1.PolicyInterface) []kyvernov1.PolicyInterface { func filterPolicies(ctx context.Context, failurePolicy string, policies ...kyvernov1.PolicyInterface) []kyvernov1.PolicyInterface {
var results []kyvernov1.PolicyInterface var results []kyvernov1.PolicyInterface
for _, policy := range policies { for _, policy := range policies {

View file

@ -3,17 +3,27 @@ package resource
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"testing" "testing"
"time" "time"
kyverno "github.com/kyverno/kyverno/api/kyverno/v1" kyverno "github.com/kyverno/kyverno/api/kyverno/v1"
kyvernov1beta1 "github.com/kyverno/kyverno/api/kyverno/v1beta1"
"github.com/kyverno/kyverno/pkg/config"
"github.com/kyverno/kyverno/pkg/engine"
enginecontext "github.com/kyverno/kyverno/pkg/engine/context"
"github.com/kyverno/kyverno/pkg/engine/jmespath"
"github.com/kyverno/kyverno/pkg/engine/policycontext"
log "github.com/kyverno/kyverno/pkg/logging" log "github.com/kyverno/kyverno/pkg/logging"
"github.com/kyverno/kyverno/pkg/policycache" "github.com/kyverno/kyverno/pkg/policycache"
"github.com/kyverno/kyverno/pkg/webhooks/handlers" "github.com/kyverno/kyverno/pkg/webhooks/handlers"
"gotest.tools/assert" "gotest.tools/assert"
admissionv1 "k8s.io/api/admission/v1"
v1 "k8s.io/api/admission/v1" v1 "k8s.io/api/admission/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" apiruntime "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/utils/pointer"
) )
var policyCheckLabel = `{ var policyCheckLabel = `{
@ -257,6 +267,121 @@ var pod = `{
} }
` `
var mutateAndGenerateMutatePolicy = `{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"name": "test-mutate"
},
"spec": {
"rules": [
{
"name": "test-mutate",
"match": {
"any": [
{
"resources": {
"kinds": [
"Pod"
],
"operations": [
"CREATE"
]
}
}
]
},
"mutate": {
"foreach": [
{
"list": "request.object.spec.containers",
"patchStrategicMerge": {
"spec": {
"containers": [
{
"name": "{{ element.name }}",
"image": "{{ regex_replace_all('^([^/]+\\.[^/]+/)?(.*)$', '{{element.image}}', 'ghcr.io/kyverno/$2' )}}"
}
]
}
}
}
]
}
}
]
}
}`
var mutateAndGenerateGeneratePolicy = `{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"name": "test-generate"
},
"spec": {
"rules": [
{
"name": "test-generate",
"match": {
"any": [
{
"resources": {
"kinds": [
"Pod"
],
"operations": [
"CREATE"
]
}
}
]
},
"generate": {
"synchronize": true,
"apiVersion": "v1",
"kind": "Pod",
"name": "pod1-{{request.name}}",
"namespace": "shared-dp",
"data": {
"spec": {
"containers": [
{
"name": "container",
"image": "nginx",
"volumeMounts": [
{
"name": "shared-volume",
"mountPath": "/data"
}
]
}
]
}
}
}
}
]
}
}`
var resourceMutateandGenerate = `{
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"name": "pod-test-1",
"namespace": "shared-dp"
},
"spec": {
"containers": [
{
"name": "container",
"image": "nginx"
}
]
}
}`
func Test_AdmissionResponseValid(t *testing.T) { func Test_AdmissionResponseValid(t *testing.T) {
policyCache := policycache.NewCache() policyCache := policycache.NewCache()
logger := log.WithName("Test_AdmissionResponseValid") logger := log.WithName("Test_AdmissionResponseValid")
@ -278,7 +403,7 @@ func Test_AdmissionResponseValid(t *testing.T) {
Operation: v1.Create, Operation: v1.Create,
Kind: metav1.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}, Kind: metav1.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"},
Resource: metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}, Resource: metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"},
Object: runtime.RawExtension{ Object: apiruntime.RawExtension{
Raw: []byte(pod), Raw: []byte(pod),
}, },
RequestResource: &metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}, RequestResource: &metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"},
@ -320,7 +445,7 @@ func Test_AdmissionResponseInvalid(t *testing.T) {
Operation: v1.Create, Operation: v1.Create,
Kind: metav1.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}, Kind: metav1.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"},
Resource: metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}, Resource: metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"},
Object: runtime.RawExtension{ Object: apiruntime.RawExtension{
Raw: []byte(pod), Raw: []byte(pod),
}, },
RequestResource: &metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}, RequestResource: &metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"},
@ -365,7 +490,7 @@ func Test_ImageVerify(t *testing.T) {
Operation: v1.Create, Operation: v1.Create,
Kind: metav1.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}, Kind: metav1.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"},
Resource: metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}, Resource: metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"},
Object: runtime.RawExtension{ Object: apiruntime.RawExtension{
Raw: []byte(pod), Raw: []byte(pod),
}, },
RequestResource: &metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}, RequestResource: &metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"},
@ -409,7 +534,7 @@ func Test_MutateAndVerify(t *testing.T) {
Operation: v1.Create, Operation: v1.Create,
Kind: metav1.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}, Kind: metav1.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"},
Resource: metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "Pod"}, Resource: metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "Pod"},
Object: runtime.RawExtension{ Object: apiruntime.RawExtension{
Raw: []byte(resourceMutateAndVerify), Raw: []byte(resourceMutateAndVerify),
}, },
RequestResource: &metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}, RequestResource: &metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"},
@ -421,6 +546,81 @@ func Test_MutateAndVerify(t *testing.T) {
assert.Equal(t, len(response.Warnings), 0) assert.Equal(t, len(response.Warnings), 0)
} }
func Test_MutateAndGenerate(t *testing.T) {
policyCache := policycache.NewCache()
logger := log.WithName("Test_MutateAndGenerate")
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
resourceHandlers := NewFakeHandlers(ctx, policyCache)
cfg := config.NewDefaultConfiguration(false)
jp := jmespath.New(cfg)
mockPcBuilder := newMockPolicyContextBuilder(cfg, jp)
resourceHandlers.pcBuilder = mockPcBuilder
var generatePolicy kyverno.ClusterPolicy
err := json.Unmarshal([]byte(mutateAndGenerateGeneratePolicy), &generatePolicy)
assert.NilError(t, err)
key := makeKey(&generatePolicy)
policyCache.Set(key, &generatePolicy, policycache.TestResourceFinder{})
var mutatePolicy kyverno.ClusterPolicy
err = json.Unmarshal([]byte(mutateAndGenerateMutatePolicy), &mutatePolicy)
assert.NilError(t, err)
key = makeKey(&mutatePolicy)
policyCache.Set(key, &mutatePolicy, policycache.TestResourceFinder{})
request := handlers.AdmissionRequest{
AdmissionRequest: v1.AdmissionRequest{
Operation: v1.Create,
Kind: metav1.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"},
Resource: metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "Pod"},
Object: apiruntime.RawExtension{
Raw: []byte(resourceMutateandGenerate),
},
RequestResource: &metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"},
DryRun: pointer.Bool(false),
},
}
response := resourceHandlers.Validate(ctx, logger, request, "", time.Now())
assert.Assert(t, len(mockPcBuilder.contexts) >= 3, fmt.Sprint("expected no of context ", 3, " received ", len(mockPcBuilder.contexts)))
validateJSONContext := mockPcBuilder.contexts[0].JSONContext()
mutateJSONContext := mockPcBuilder.contexts[1].JSONContext()
generateJSONContext := mockPcBuilder.contexts[2].JSONContext()
_, err = enginecontext.AddMockDeferredLoader(validateJSONContext, "key1", "value1")
assert.NilError(t, err)
_, err = enginecontext.AddMockDeferredLoader(mutateJSONContext, "key2", "value2")
assert.NilError(t, err)
_, err = enginecontext.AddMockDeferredLoader(generateJSONContext, "key3", "value3")
assert.NilError(t, err)
_, err = mutateJSONContext.Query("key1")
assert.ErrorContains(t, err, `Unknown key "key1" in path`)
_, err = generateJSONContext.Query("key1")
assert.ErrorContains(t, err, `Unknown key "key1" in path`)
_, err = validateJSONContext.Query("key2")
assert.ErrorContains(t, err, `Unknown key "key2" in path`)
_, err = generateJSONContext.Query("key2")
assert.ErrorContains(t, err, `Unknown key "key2" in path`)
_, err = validateJSONContext.Query("key3")
assert.ErrorContains(t, err, `Unknown key "key3" in path`)
_, err = mutateJSONContext.Query("key3")
assert.ErrorContains(t, err, `Unknown key "key3" in path`)
assert.Equal(t, response.Allowed, true)
assert.Equal(t, len(response.Warnings), 0)
}
func makeKey(policy kyverno.PolicyInterface) string { func makeKey(policy kyverno.PolicyInterface) string {
name := policy.GetName() name := policy.GetName()
namespace := policy.GetNamespace() namespace := policy.GetNamespace()
@ -430,3 +630,37 @@ func makeKey(policy kyverno.PolicyInterface) string {
return namespace + "/" + name return namespace + "/" + name
} }
type mockPolicyContextBuilder struct {
configuration config.Configuration
jp jmespath.Interface
contexts []*engine.PolicyContext
count int
}
func newMockPolicyContextBuilder(
configuration config.Configuration,
jp jmespath.Interface,
) *mockPolicyContextBuilder {
return &mockPolicyContextBuilder{
configuration: configuration,
jp: jp,
contexts: make([]*policycontext.PolicyContext, 0),
count: 0,
}
}
func (b *mockPolicyContextBuilder) Build(request admissionv1.AdmissionRequest, roles, clusterRoles []string, gvk schema.GroupVersionKind) (*engine.PolicyContext, error) {
userRequestInfo := kyvernov1beta1.RequestInfo{
AdmissionUserInfo: *request.UserInfo.DeepCopy(),
Roles: roles,
ClusterRoles: clusterRoles,
}
pc, err := engine.NewPolicyContextFromAdmissionRequest(b.jp, request, userRequestInfo, gvk, b.configuration)
if err != nil {
return nil, err
}
b.count += 1
b.contexts = append(b.contexts, pc)
return pc, err
}

View file

@ -9,25 +9,31 @@ import (
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
kyvernov1beta1 "github.com/kyverno/kyverno/api/kyverno/v1beta1" kyvernov1beta1 "github.com/kyverno/kyverno/api/kyverno/v1beta1"
"github.com/kyverno/kyverno/pkg/autogen" "github.com/kyverno/kyverno/pkg/autogen"
"github.com/kyverno/kyverno/pkg/engine"
engineapi "github.com/kyverno/kyverno/pkg/engine/api" engineapi "github.com/kyverno/kyverno/pkg/engine/api"
"github.com/kyverno/kyverno/pkg/event" "github.com/kyverno/kyverno/pkg/event"
datautils "github.com/kyverno/kyverno/pkg/utils/data" datautils "github.com/kyverno/kyverno/pkg/utils/data"
"github.com/kyverno/kyverno/pkg/webhooks/handlers"
"github.com/kyverno/kyverno/pkg/webhooks/resource/generation" "github.com/kyverno/kyverno/pkg/webhooks/resource/generation"
webhookutils "github.com/kyverno/kyverno/pkg/webhooks/utils" webhookutils "github.com/kyverno/kyverno/pkg/webhooks/utils"
admissionv1 "k8s.io/api/admission/v1" admissionv1 "k8s.io/api/admission/v1"
) )
// handleBackgroundApplies applies generate and mutateExisting policies, and creates update requests for background reconcile // handleBackgroundApplies applies generate and mutateExisting policies, and creates update requests for background reconcile
func (h *resourceHandlers) handleBackgroundApplies(ctx context.Context, logger logr.Logger, request admissionv1.AdmissionRequest, policyContext *engine.PolicyContext, generatePolicies, mutatePolicies []kyvernov1.PolicyInterface, ts time.Time) { func (h *resourceHandlers) handleBackgroundApplies(ctx context.Context, logger logr.Logger, request handlers.AdmissionRequest, generatePolicies, mutatePolicies []kyvernov1.PolicyInterface, ts time.Time) {
go h.handleMutateExisting(ctx, logger, request, mutatePolicies, policyContext, ts) h.wg.Add(1)
h.handleGenerate(ctx, logger, request, generatePolicies, policyContext, ts) go h.handleMutateExisting(ctx, logger, request, mutatePolicies, ts)
h.handleGenerate(ctx, logger, request, generatePolicies, ts)
} }
func (h *resourceHandlers) handleMutateExisting(ctx context.Context, logger logr.Logger, request admissionv1.AdmissionRequest, policies []kyvernov1.PolicyInterface, polCtx *engine.PolicyContext, admissionRequestTimestamp time.Time) { func (h *resourceHandlers) handleMutateExisting(ctx context.Context, logger logr.Logger, request handlers.AdmissionRequest, policies []kyvernov1.PolicyInterface, admissionRequestTimestamp time.Time) {
policyContext := &engine.PolicyContext{} policyContext, err := h.buildPolicyContextFromAdmissionRequest(logger, request)
*policyContext = *polCtx if err != nil {
if request.Operation == admissionv1.Delete { logger.Error(err, "failed to create policy context")
return
}
h.wg.Done()
if request.AdmissionRequest.Operation == admissionv1.Delete {
policyContext = policyContext.WithNewResource(policyContext.OldResource()) policyContext = policyContext.WithNewResource(policyContext.OldResource())
} }
@ -46,7 +52,7 @@ func (h *resourceHandlers) handleMutateExisting(ctx context.Context, logger logr
// skip rules that don't specify the DELETE operation in case the admission request is of type DELETE // skip rules that don't specify the DELETE operation in case the admission request is of type DELETE
var skipped []string var skipped []string
for _, rule := range autogen.ComputeRules(policy) { for _, rule := range autogen.ComputeRules(policy) {
if request.Operation == admissionv1.Delete && !webhookutils.MatchDeleteOperation(rule) { if request.AdmissionRequest.Operation == admissionv1.Delete && !webhookutils.MatchDeleteOperation(rule) {
skipped = append(skipped, rule.Name) skipped = append(skipped, rule.Name)
} }
} }
@ -67,7 +73,7 @@ func (h *resourceHandlers) handleMutateExisting(ctx context.Context, logger logr
} }
} }
if failedResponse := applyUpdateRequest(ctx, request, kyvernov1beta1.Mutate, h.urGenerator, policyContext.AdmissionInfo(), request.Operation, engineResponses...); failedResponse != nil { if failedResponse := applyUpdateRequest(ctx, request.AdmissionRequest, kyvernov1beta1.Mutate, h.urGenerator, policyContext.AdmissionInfo(), request.Operation, engineResponses...); failedResponse != nil {
for _, failedUR := range failedResponse { for _, failedUR := range failedResponse {
err := fmt.Errorf("failed to create update request: %v", failedUR.err) err := fmt.Errorf("failed to create update request: %v", failedUR.err)
@ -86,7 +92,14 @@ func (h *resourceHandlers) handleMutateExisting(ctx context.Context, logger logr
} }
} }
func (h *resourceHandlers) handleGenerate(ctx context.Context, logger logr.Logger, request admissionv1.AdmissionRequest, generatePolicies []kyvernov1.PolicyInterface, policyContext *engine.PolicyContext, ts time.Time) { func (h *resourceHandlers) handleGenerate(ctx context.Context, logger logr.Logger, request handlers.AdmissionRequest, generatePolicies []kyvernov1.PolicyInterface, ts time.Time) {
policyContext, err := h.buildPolicyContextFromAdmissionRequest(logger, request)
if err != nil {
logger.Error(err, "failed to create policy context")
return
}
h.wg.Done()
gh := generation.NewGenerationHandler(logger, h.engine, h.client, h.kyvernoClient, h.nsLister, h.urLister, h.cpolLister, h.polLister, h.urGenerator, h.eventGen, h.metricsConfig, h.backgroundServiceAccountName) gh := generation.NewGenerationHandler(logger, h.engine, h.client, h.kyvernoClient, h.nsLister, h.urLister, h.cpolLister, h.polLister, h.urGenerator, h.eventGen, h.metricsConfig, h.backgroundServiceAccountName)
var policies []kyvernov1.PolicyInterface var policies []kyvernov1.PolicyInterface
for _, p := range generatePolicies { for _, p := range generatePolicies {
@ -95,5 +108,5 @@ func (h *resourceHandlers) handleGenerate(ctx context.Context, logger logr.Logge
policies = append(policies, new) policies = append(policies, new)
} }
} }
go gh.Handle(ctx, request, policies, policyContext) go gh.Handle(ctx, request.AdmissionRequest, policies, policyContext)
} }

View file

@ -0,0 +1,6 @@
# Title
This is a generate test to ensure that when there is a generate and a mutate policy present, the deferred loader does not panic because of concurrency issues in the policy context.
# Related Issue
https://github.com/kyverno/kyverno/issues/9413

View file

@ -0,0 +1,70 @@
apiVersion: chainsaw.kyverno.io/v1alpha1
kind: Test
metadata:
creationTimestamp: null
name: cpol-data-sync-create
spec:
steps:
- name: step-01
try:
- apply:
file: ns.yaml
- name: step-02
try:
- apply:
file: first-pod.yaml
- name: step-03
try:
- apply:
file: policies.yaml
- name: step-04
try:
- apply:
file: pod1.yaml
- name: step-05 # checks if admission controller was restarted
try:
- script:
content: kubectl get po -A | awk '$5>0' | grep -q 'kyverno-admission-controller'
check:
# there should not be any matching value thus error != null is true
($error != null): true
- name: step-06
try:
- apply:
file: pod2.yaml
- name: step-07
try:
- script:
content: kubectl get po -A | awk '$5>0' | grep -q 'kyverno-admission-controller'
check:
($error != null): true
- name: step-08
try:
- apply:
file: pod3.yaml
- name: step-09
try:
- script:
content: kubectl get po -A | awk '$5>0' | grep -q 'kyverno-admission-controller'
check:
($error != null): true
- name: step-10
try:
- apply:
file: pod4.yaml
- name: step-11
try:
- script:
content: kubectl get po -A | awk '$5>0' | grep -q 'kyverno-admission-controller'
check:
($error != null): true
- name: step-12
try:
- apply:
file: pod5.yaml
- name: step-13
try:
- script:
content: kubectl get po -A | awk '$5>0' | grep -q 'kyverno-admission-controller'
check:
($error != null): true

View file

@ -0,0 +1,9 @@
apiVersion: v1
kind: Pod
metadata:
name: first-pod
namespace: shared-dp
spec:
containers:
- name: container
image: nginx

View file

@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: shared-dp

View file

@ -0,0 +1,9 @@
apiVersion: v1
kind: Pod
metadata:
name: pod-test-1
namespace: shared-dp
spec:
containers:
- name: container
image: nginx

View file

@ -0,0 +1,11 @@
apiVersion: v1
kind: Pod
metadata:
name: pod-test-2
namespace: shared-dp
spec:
containers:
- name: container
image: nginx

View file

@ -0,0 +1,9 @@
apiVersion: v1
kind: Pod
metadata:
name: pod-test-3
namespace: shared-dp
spec:
containers:
- name: container
image: nginx

View file

@ -0,0 +1,9 @@
apiVersion: v1
kind: Pod
metadata:
name: pod-test-4
namespace: shared-dp
spec:
containers:
- name: container
image: nginx

View file

@ -0,0 +1,9 @@
apiVersion: v1
kind: Pod
metadata:
name: pod-test-5
namespace: shared-dp
spec:
containers:
- name: container
image: nginx

View file

@ -0,0 +1,72 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: test-mutate
spec:
rules:
- name: test-mutate
match:
any:
- resources:
kinds:
- Pod
operations:
- CREATE
context:
- name: list_pods
apiCall:
urlPath: "/api/v1/namespaces/{{request.namespace}}/pods"
jmesPath: "items[]"
preconditions:
all:
- key: "{{ length(list_pods) }}"
operator: GreaterThan
value: 0
mutate:
targets:
- apiVersion: v1
kind: Pod
name: "{{ list_pods[0].metadata.name }}"
patchesJson6902: |-
- path: "/spec/priority"
op: add
value: 1000
---
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: test-generate
spec:
rules:
- name: test-generate
match:
any:
- resources:
kinds:
- Pod
operations:
- CREATE
context:
- name: list_pods
apiCall:
urlPath: "/api/v1/namespaces/{{request.namespace}}/pods"
jmesPath: "items[]"
preconditions:
all:
- key: "{{ length(list_pods) }}"
operator: GreaterThan
value: 0
generate:
synchronize: true
apiVersion: v1
kind: Pod
name: pod1-{{request.name}}
namespace: shared-dp
data:
spec:
containers:
- name: container
image: nginx
volumeMounts:
- name: shared-volume
mountPath: /data