1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2024-12-14 11:57:48 +00:00

Fix deferred loading (#7597)

* handle nested contexts

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* add feature flag

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* fix tests

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* add kuttl tests

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* fix linter issues

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* fix CLI regclient

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* fix: token permissions on report vulns workflow (#7611)

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* fix: token permissions (#7619)

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* fix: update the flag descriptions of the reports-controller (#7617)

Signed-off-by: emmanuel-ferdman <emmanuelferdman@gmail.com>

* fix: panic if env var not defined (#7613)

* fix: panic if env var not defined

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* fix

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* fix

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

---------

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* use toggles instead of a flag

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* update toggle name

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* update toggle name

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* fix roles

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* fix role

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* update manifests

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* remove extra unlock

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* fix loader reset

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* add tests

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* propagate context

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* cm resolver

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* level management

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* address review comments

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* add enableDeferredLoading to other controllers

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* re-enable ACR credhelper

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* improve tests

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* remove image registry client init

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* check for invalid reset/restore

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* recursive kuttl test

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* add pre/post queries

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* add check for a recursive match

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* new test suite

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* eval loaders at creation level

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* kuttl test

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* add an index for resolving deps in order

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* improve comment

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* extract remove method

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* merge main

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* flags

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* feature flag

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* fix flag

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* update unit tests

Signed-off-by: ShutingZhao <shuting@nirmata.com>

* two rules kuttl test

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* update unit tests

Signed-off-by: ShutingZhao <shuting@nirmata.com>

* revert

Signed-off-by: ShutingZhao <shuting@nirmata.com>

* per rule checkpoint

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* fix

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* fix mutate chained rules

Signed-off-by: ShutingZhao <shuting@nirmata.com>

* per rule checpoint/restore

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* log error

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

---------

Signed-off-by: Jim Bugwadia <jim@nirmata.com>
Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>
Signed-off-by: emmanuel-ferdman <emmanuelferdman@gmail.com>
Signed-off-by: ShutingZhao <shuting@nirmata.com>
Co-authored-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>
Co-authored-by: Emmanuel Ferdman <emmanuelferdman@gmail.com>
Co-authored-by: shuting <shuting@nirmata.com>
This commit is contained in:
Jim Bugwadia 2023-06-27 09:58:50 -07:00 committed by GitHub
parent df5f54198d
commit b98c0775f2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 1165 additions and 71 deletions

View file

@ -58,6 +58,7 @@ jobs:
tests:
- autogen
- cleanup
- deferred
- events
- exceptions
- generate/clusterpolicy

View file

@ -653,7 +653,7 @@ test-cli-local: $(CLI_BIN)
.PHONY: test-cli-local-mutate
test-cli-local-mutate: $(CLI_BIN)
# @$(CLI_BIN) test ./test/cli/test-mutate
@$(CLI_BIN) test ./test/cli/test-mutate
.PHONY: test-cli-local-generate
test-cli-local-generate: $(CLI_BIN)

View file

@ -113,17 +113,32 @@ func (h *handlers) lookupPolicy(namespace, name string) (kyvernov2alpha1.Cleanup
}
}
func (h *handlers) executePolicy(ctx context.Context, logger logr.Logger, policy kyvernov2alpha1.CleanupPolicyInterface, cfg config.Configuration) error {
func (h *handlers) executePolicy(
ctx context.Context,
logger logr.Logger,
policy kyvernov2alpha1.CleanupPolicyInterface,
cfg config.Configuration,
) error {
spec := policy.GetSpec()
kinds := sets.New(spec.MatchResources.GetKinds()...)
debug := logger.V(4)
var errs []error
enginectx := enginecontext.NewContext(h.jp)
factory := factories.DefaultContextLoaderFactory(h.cmResolver)
loader := factory(nil, kyvernov1.Rule{})
if err := loader.Load(ctx, h.jp, h.client, nil, spec.Context, enginectx); err != nil {
ctxFactory := factories.DefaultContextLoaderFactory(h.cmResolver)
loader := ctxFactory(nil, kyvernov1.Rule{})
if err := loader.Load(
ctx,
h.jp,
h.client,
nil,
spec.Context,
enginectx,
); err != nil {
return err
}
for kind := range kinds {
commonLabels := []attribute.KeyValue{
attribute.String("policy_type", policy.GetKind()),

View file

@ -144,7 +144,7 @@ func KyvernoUserName(serviceaccount string) string {
type Configuration interface {
// GetDefaultRegistry return default image registry
GetDefaultRegistry() string
// GetEnableDefaultRegistryMutation return if should mutate image registry
// GetEnableDefaultRegistryMutation returns true if image references should be mutated
GetEnableDefaultRegistryMutation() bool
// IsExcluded checks exlusions/inclusions to determine if the admission request should be excluded or not
IsExcluded(username string, groups []string, roles []string, clusterroles []string) bool

View file

@ -130,6 +130,7 @@ func Test_namespacedResourceResolverChain_Get(t *testing.T) {
if !checkError(tt.wantErr, err) {
t.Errorf("ConfigmapResolver.Get() %s error = %v, wantErr %v", tt.name, err, tt.wantErr)
}
if !reflect.DeepEqual(got, tt.wantCm) {
t.Errorf("ConfigmapResolver.Get() = %v, want %v", got, tt.wantCm)
}

View file

@ -20,6 +20,7 @@ import (
var logger = logging.WithName("context")
// EvalInterface is used to query and inspect context data
// TODO: move to contextapi to prevent circular dependencies
type EvalInterface interface {
// Query accepts a JMESPath expression and returns matching data
Query(query string) (interface{}, error)
@ -32,6 +33,7 @@ type EvalInterface interface {
}
// Interface to manage context operations
// TODO: move to contextapi to prevent circular dependencies
type Interface interface {
// AddRequest marshals and adds the admission request to the context
AddRequest(request admissionv1.AdmissionRequest) error
@ -76,7 +78,8 @@ type Interface interface {
AddImageInfos(resource *unstructured.Unstructured, cfg config.Configuration) error
// AddDeferredLoader adds a loader that is executed on first use (query)
AddDeferredLoader(name string, loader Loader) error
// If deferred loading is disabled the loader is immediately executed.
AddDeferredLoader(loader DeferredLoader) error
// ImageInfo returns image infos present in the context
ImageInfo() map[string]map[string]apiutils.ImageInfo
@ -107,12 +110,7 @@ type context struct {
jsonRaw []byte
jsonRawCheckpoints [][]byte
images map[string]map[string]apiutils.ImageInfo
deferred deferredLoaders
}
type deferredLoaders struct {
mutex sync.Mutex
loaders map[string]Loader
deferred DeferredLoaders
}
// NewContext returns a new context
@ -126,9 +124,7 @@ func NewContextFromRaw(jp jmespath.Interface, raw []byte) Interface {
jp: jp,
jsonRaw: raw,
jsonRawCheckpoints: make([][]byte, 0),
deferred: deferredLoaders{
loaders: make(map[string]Loader),
},
deferred: NewDeferredLoaders(),
}
}
@ -335,24 +331,32 @@ func (ctx *context) Reset() {
ctx.reset(false)
}
func (ctx *context) reset(remove bool) {
func (ctx *context) reset(restore bool) {
if ctx.resetCheckpoint(restore) {
ctx.deferred.Reset(restore, len(ctx.jsonRawCheckpoints))
}
}
func (ctx *context) resetCheckpoint(removeCheckpoint bool) bool {
ctx.mutex.Lock()
defer ctx.mutex.Unlock()
if len(ctx.jsonRawCheckpoints) == 0 {
return
return false
}
n := len(ctx.jsonRawCheckpoints) - 1
jsonRawCheckpoint := ctx.jsonRawCheckpoints[n]
ctx.jsonRaw = make([]byte, len(jsonRawCheckpoint))
copy(ctx.jsonRaw, jsonRawCheckpoint)
if remove {
if removeCheckpoint {
ctx.jsonRawCheckpoints = ctx.jsonRawCheckpoints[:n]
}
return true
}
func (ctx *context) AddDeferredLoader(name string, loader Loader) error {
ctx.deferred.mutex.Lock()
defer ctx.deferred.mutex.Unlock()
ctx.deferred.loaders[name] = loader
func (ctx *context) AddDeferredLoader(dl DeferredLoader) error {
ctx.deferred.Add(dl, len(ctx.jsonRawCheckpoints))
return nil
}

View file

@ -0,0 +1,177 @@
package context
import (
"regexp"
)
type deferredLoader struct {
name string
matcher regexp.Regexp
loader Loader
}
func NewDeferredLoader(name string, loader Loader) (DeferredLoader, error) {
// match on ASCII word boundaries except do not allow starting with a `.`
// this allows `x` to match `x.y` but not `y.x` or `y.x.z`
matcher, err := regexp.Compile(`(?:\A|\z|\s|[^.0-9A-Za-z])` + name + `\b`)
if err != nil {
return nil, err
}
return &deferredLoader{
name: name,
matcher: *matcher,
loader: loader,
}, nil
}
func (dl *deferredLoader) Name() string {
return dl.name
}
func (dl *deferredLoader) HasLoaded() bool {
return dl.loader.HasLoaded()
}
func (dl *deferredLoader) LoadData() error {
return dl.loader.LoadData()
}
func (d *deferredLoader) Matches(query string) bool {
return d.matcher.MatchString(query)
}
type leveledLoader struct {
level int
matched bool
loader DeferredLoader
}
func (cl *leveledLoader) Level() int {
return cl.level
}
func (cl *leveledLoader) Name() string {
return cl.loader.Name()
}
func (cl *leveledLoader) Matches(query string) bool {
return cl.loader.Matches(query)
}
func (cl *leveledLoader) HasLoaded() bool {
return cl.loader.HasLoaded()
}
func (cl *leveledLoader) LoadData() error {
return cl.loader.LoadData()
}
type deferredLoaders struct {
level int
index int
loaders []*leveledLoader
}
func NewDeferredLoaders() DeferredLoaders {
return &deferredLoaders{
loaders: make([]*leveledLoader, 0),
level: -1,
index: -1,
}
}
func (d *deferredLoaders) Add(dl DeferredLoader, level int) {
d.loaders = append(d.loaders, &leveledLoader{level, false, dl})
}
func (d *deferredLoaders) Reset(restore bool, level int) {
d.clearMatches()
for i := 0; i < len(d.loaders); i++ {
l := d.loaders[i]
if l.level > level {
i = d.removeLoader(i)
} else {
if l.loader.HasLoaded() {
// reload data into the current context for restore, and
// for a reset but only if loader is at a prior level
if restore || (l.level < level) {
if err := d.loadData(l, i); err != nil {
logger.Error(err, "failed to reload context entry", "name", l.loader.Name())
}
}
if l.level == level {
i = d.removeLoader(i)
}
} else if !restore {
if l.level == level {
i = d.removeLoader(i)
}
}
}
}
}
// removeLoader removes loader at the specified index
// and returns the prior index
func (d *deferredLoaders) removeLoader(i int) int {
d.loaders = append(d.loaders[:i], d.loaders[i+1:]...)
return i - 1
}
func (d *deferredLoaders) clearMatches() {
for _, dl := range d.loaders {
dl.matched = false
}
}
func (d *deferredLoaders) LoadMatching(query string, level int) error {
if d.level >= 0 {
level = d.level
}
index := len(d.loaders)
if d.index >= 0 {
index = d.index
}
for l, idx := d.match(query, level, index); l != nil; l, idx = d.match(query, level, index) {
if err := d.loadData(l, idx); err != nil {
return nil
}
}
return nil
}
func (d *deferredLoaders) loadData(l *leveledLoader, index int) error {
d.setLevelAndIndex(l.level, index)
defer d.setLevelAndIndex(-1, -1)
if err := l.LoadData(); err != nil {
return err
}
return nil
}
func (d *deferredLoaders) setLevelAndIndex(level, index int) {
d.level = level
d.index = index
}
func (d *deferredLoaders) match(query string, level, index int) (*leveledLoader, int) {
for i := 0; i < index; i++ {
dl := d.loaders[i]
if dl.matched || dl.loader.HasLoaded() {
continue
}
if dl.Matches(query) && dl.level <= level {
idx := i
d.loaders[i].matched = true
return dl, idx
}
}
return nil, -1
}

View file

@ -0,0 +1,443 @@
package context
import (
"fmt"
"testing"
"gotest.tools/assert"
)
func TestDeferredLoaderMatch(t *testing.T) {
ctx := newContext()
mockLoader, _ := addDeferred(ctx, "one", "1")
assert.Equal(t, 0, mockLoader.invocations)
val, err := ctx.Query("one")
assert.NilError(t, err)
assert.Equal(t, "1", val)
assert.Equal(t, 1, mockLoader.invocations)
_, _ = ctx.Query("one")
assert.Equal(t, 1, mockLoader.invocations)
ctx = newContext()
ml, _ := addDeferred(ctx, "one", "1")
testCheckMatch(t, ctx, "one<two", "one", "1", ml)
ml, _ = addDeferred(ctx, "one", "1")
testCheckMatch(t, ctx, "(one)", "one", "1", ml)
ml, _ = addDeferred(ctx, "one", "1")
testCheckMatch(t, ctx, "one.two.three", "one", "1", ml)
ml, _ = addDeferred(ctx, "one", "1")
testCheckMatch(t, ctx, "one-two", "one", "1", ml)
ml, _ = addDeferred(ctx, "one", "1")
testCheckMatch(t, ctx, "one; two; three", "one", "1", ml)
ml, _ = addDeferred(ctx, "one", "1")
testCheckMatch(t, ctx, "one>two", "one", "1", ml)
ml, _ = addDeferred(ctx, "one", "1")
testCheckMatch(t, ctx, "one, two, three", "one", "1", ml)
ml, _ = addDeferred(ctx, "one1", "11")
testCheckMatch(t, ctx, "one1", "one1", "11", ml)
}
func testCheckMatch(t *testing.T, ctx *context, query, name, value string, ml *mockLoader) {
var events []string
hdlr := func(name string) {
events = append(events, name)
}
ml.setEventHandler(hdlr)
err := ctx.deferred.LoadMatching(query, len(ctx.jsonRawCheckpoints))
assert.NilError(t, err)
assert.Equal(t, 1, len(events), "deferred loader %s not executed for query %s", name, query)
expected := fmt.Sprintf("%s=%s", name, value)
assert.Equal(t, expected, events[0], "deferred loader %s name mismatch for query %s; received %s", name, query, events[0])
}
func TestDeferredLoaderMismatch(t *testing.T) {
ctx := newContext()
addDeferred(ctx, "one", "1")
_, err := ctx.Query("oneTwoThree")
assert.ErrorContains(t, err, `Unknown key "oneTwoThree" in path`)
_, err = ctx.Query("one1")
assert.ErrorContains(t, err, `Unknown key "one1" in path`)
_, err = ctx.Query("one_two")
assert.ErrorContains(t, err, `Unknown key "one_two" in path`)
_, err = ctx.Query("\"one-two\"")
assert.ErrorContains(t, err, `Unknown key "one-two" in path`)
ctx.AddVariable("two.one", "0")
val, err := ctx.Query("two.one")
assert.NilError(t, err)
assert.Equal(t, "0", val)
val, err = ctx.Query("one.two.three")
assert.NilError(t, err)
assert.Equal(t, nil, val)
val, err = ctx.Query("one")
assert.NilError(t, err)
assert.Equal(t, "1", val)
}
func newContext() *context {
return &context{
jp: jp,
jsonRaw: []byte(`{}`),
jsonRawCheckpoints: make([][]byte, 0),
deferred: NewDeferredLoaders(),
}
}
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)
if err != nil {
return loader, err
}
ctx.AddDeferredLoader(d)
return loader, nil
}
func TestDeferredReset(t *testing.T) {
ctx := newContext()
addDeferred(ctx, "value", "0")
ctx.Checkpoint()
val, err := ctx.Query("value")
assert.NilError(t, err)
assert.Equal(t, "0", val)
ctx.Reset()
val, err = ctx.Query("value")
assert.NilError(t, err)
assert.Equal(t, "0", val)
}
func TestDeferredCheckpointRestore(t *testing.T) {
ctx := newContext()
ctx.Checkpoint()
unused, _ := addDeferred(ctx, "unused", "unused")
mock, _ := addDeferred(ctx, "one", "1")
ctx.Restore()
assert.Equal(t, 0, mock.invocations)
assert.Equal(t, 0, unused.invocations)
err := ctx.deferred.LoadMatching("unused", len(ctx.jsonRawCheckpoints))
assert.NilError(t, err)
_, err = ctx.Query("unused")
assert.ErrorContains(t, err, "Unknown key \"unused\" in path")
err = ctx.deferred.LoadMatching("one", len(ctx.jsonRawCheckpoints))
assert.NilError(t, err)
_, err = ctx.Query("one")
assert.ErrorContains(t, err, "Unknown key \"one\" in path")
_, _ = addDeferred(ctx, "one", "1")
ctx.Checkpoint()
one, err := ctx.Query("one")
assert.NilError(t, err)
assert.Equal(t, "1", one)
ctx.Restore()
_, err = ctx.Query("one")
assert.NilError(t, err)
assert.Equal(t, "1", one)
ctx.Restore()
_, err = ctx.Query("one")
assert.NilError(t, err)
assert.Equal(t, "1", one)
mock, _ = addDeferred(ctx, "one", "1")
ctx.Checkpoint()
val, err := ctx.Query("one")
assert.NilError(t, err)
assert.Equal(t, "1", val)
assert.Equal(t, 1, mock.invocations)
mock2, _ := addDeferred(ctx, "two", "2")
val, err = ctx.Query("two")
assert.NilError(t, err)
assert.Equal(t, "2", val)
assert.Equal(t, 1, mock2.invocations)
ctx.Restore()
val, err = ctx.Query("one")
assert.NilError(t, err)
assert.Equal(t, "1", val)
assert.Equal(t, 2, mock.invocations)
_, _ = ctx.Query("one")
assert.Equal(t, 2, mock.invocations)
_, err = ctx.Query("two")
assert.ErrorContains(t, err, `Unknown key "two" in path`)
ctx.Checkpoint()
val, err = ctx.Query("one")
assert.NilError(t, err)
assert.Equal(t, "1", val)
assert.Equal(t, 2, mock.invocations)
_, err = ctx.Query("two")
assert.ErrorContains(t, err, `Unknown key "two" in path`)
mock3, _ := addDeferred(ctx, "three", "3")
val, err = ctx.Query("three")
assert.NilError(t, err)
assert.Equal(t, "3", val)
assert.Equal(t, 1, mock3.invocations)
ctx.Reset()
val, err = ctx.Query("one")
assert.NilError(t, err)
assert.Equal(t, "1", val)
assert.Equal(t, 2, mock.invocations)
_, err = ctx.Query("two")
assert.ErrorContains(t, err, `Unknown key "two" in path`)
_, err = ctx.Query("three")
assert.ErrorContains(t, err, `Unknown key "three" in path`)
}
func TestDeferredForloop(t *testing.T) {
ctx := newContext()
addDeferred(ctx, "value", -1)
ctx.Checkpoint()
for i := 0; i < 5; i++ {
val, err := ctx.Query("value")
assert.NilError(t, err)
assert.Equal(t, float64(i-1), val)
ctx.Reset()
mock, _ := addDeferred(ctx, "value", i)
val, err = ctx.Query("value")
assert.NilError(t, err)
assert.Equal(t, float64(i), val)
assert.Equal(t, 1, mock.invocations)
}
ctx.Restore()
val, err := ctx.Query("value")
assert.NilError(t, err)
assert.Equal(t, float64(-1), val)
}
func TestDeferredInvalidReset(t *testing.T) {
ctx := newContext()
addDeferred(ctx, "value", "0")
ctx.Reset() // no checkpoint
val, err := ctx.Query("value")
assert.NilError(t, err)
assert.Equal(t, "0", val)
addDeferred(ctx, "value", "0")
ctx.Restore() // no checkpoint
val, err = ctx.Query("value")
assert.NilError(t, err)
assert.Equal(t, "0", val)
}
func TestDeferredValidResetRestore(t *testing.T) {
ctx := newContext()
addDeferred(ctx, "value", "0")
ctx.Checkpoint()
addDeferred(ctx, "leak", "leak")
ctx.Reset()
_, err := ctx.Query("leak")
assert.ErrorContains(t, err, `Unknown key "leak" in path`)
addDeferred(ctx, "value", "0")
ctx.Checkpoint()
addDeferred(ctx, "leak", "leak")
ctx.Restore()
_, err = ctx.Query("leak")
assert.ErrorContains(t, err, `Unknown key "leak" in path`)
}
func TestDeferredSameName(t *testing.T) {
ctx := newContext()
var sequence []string
hdlr := func(name string) {
sequence = append(sequence, name)
}
mock1, _ := addDeferred(ctx, "value", "0")
mock1.setEventHandler(hdlr)
mock2, _ := addDeferred(ctx, "value", "1")
mock2.setEventHandler(hdlr)
val, err := ctx.Query("value")
assert.NilError(t, err)
assert.Equal(t, "1", val)
assert.Equal(t, 1, mock1.invocations)
assert.Equal(t, 1, mock2.invocations)
assert.Equal(t, 2, len(sequence))
assert.Equal(t, sequence[0], "value=0")
assert.Equal(t, sequence[1], "value=1")
}
func TestDeferredRecursive(t *testing.T) {
ctx := newContext()
addDeferredWithQuery(ctx, "value", "0", "value")
ctx.Checkpoint()
val, err := ctx.Query("value")
assert.NilError(t, err)
assert.Equal(t, "0", val)
}
func TestJMESPathDependency(t *testing.T) {
ctx := newContext()
addDeferred(ctx, "foo", "foo")
addDeferredWithQuery(ctx, "one", "1", "foo")
val, err := ctx.Query("one")
assert.NilError(t, err)
assert.Equal(t, "foo", val)
}
func TestDeferredHiddenEval(t *testing.T) {
ctx := newContext()
addDeferred(ctx, "foo", "foo")
ctx.Checkpoint()
addDeferred(ctx, "foo", "bar")
val, err := ctx.Query("foo")
assert.NilError(t, err)
assert.Equal(t, "bar", val)
}
func TestDeferredNotHidden(t *testing.T) {
ctx := newContext()
addDeferred(ctx, "foo", "foo")
addDeferredWithQuery(ctx, "one", "1", "foo")
ctx.Checkpoint()
addDeferred(ctx, "foo", "bar")
val, err := ctx.Query("one")
assert.NilError(t, err)
assert.Equal(t, "foo", val)
}
func TestDeferredNotHiddenOrdered(t *testing.T) {
ctx := newContext()
addDeferred(ctx, "foo", "foo")
addDeferredWithQuery(ctx, "one", "1", "foo")
addDeferred(ctx, "foo", "baz")
ctx.Checkpoint()
addDeferred(ctx, "foo", "bar")
val, err := ctx.Query("one")
assert.NilError(t, err)
assert.Equal(t, "foo", val)
val, err = ctx.Query("foo")
assert.NilError(t, err)
assert.Equal(t, "bar", val)
ctx.Restore()
val, err = ctx.Query("one")
assert.NilError(t, err)
assert.Equal(t, "foo", val)
val, err = ctx.Query("foo")
assert.NilError(t, err)
assert.Equal(t, "baz", val)
}

View file

@ -39,35 +39,8 @@ func (ctx *context) Query(query string) (interface{}, error) {
}
func (ctx *context) loadDeferred(query string) error {
loaders := ctx.getMatchingLoaders(query)
for _, loader := range loaders {
err := ctx.evaluateLoader(loader)
if err != nil {
return err
}
}
return nil
}
func (ctx *context) getMatchingLoaders(query string) []string {
ctx.deferred.mutex.Lock()
defer ctx.deferred.mutex.Unlock()
var matchingLoaders []string
for name := range ctx.deferred.loaders {
if strings.Contains(query, name) {
matchingLoaders = append(matchingLoaders, name)
}
}
return matchingLoaders
}
func (ctx *context) evaluateLoader(name string) error {
loader, ok := ctx.deferred.loaders[name]
if !ok {
return nil
}
delete(ctx.deferred.loaders, name)
return loader.LoadData()
level := len(ctx.jsonRawCheckpoints)
return ctx.deferred.LoadMatching(query, level)
}
func (ctx *context) HasChanged(jmespath string) (bool, error) {

View file

@ -14,3 +14,30 @@ type Loader interface {
// executed and stored data in a context
HasLoaded() bool
}
// DeferredLoader wraps a Loader and implements context specific behaviors.
// A `level` is used to track the checkpoint level at which the loader was
// created. If the level when loading occurs matches the loader's creation
// level, the loader is discarded after execution. Otherwise, the loader is
// retained so that it can be applied to the prior level when the checkpoint
// is restored or reset.
type DeferredLoader interface {
Name() string
Matches(query string) bool
HasLoaded() bool
LoadData() error
}
// LeveledLoader is a DeferredLoader with a Level
type LeveledLoader interface {
// Level provides the declaration level for the DeferredLoader
Level() int
DeferredLoader
}
// DeferredLoaders manages a list of DeferredLoader instances
type DeferredLoaders interface {
Add(loader DeferredLoader, level int)
LoadMatching(query string, level int) error
Reset(removeCheckpoint bool, level int)
}

View file

@ -240,7 +240,7 @@ func (e *engine) invokeRuleHandler(
ctx,
"pkg/engine",
fmt.Sprintf("RULE %s", rule.Name),
func(ctx context.Context, span trace.Span) (unstructured.Unstructured, []engineapi.RuleResponse) {
func(ctx context.Context, span trace.Span) (patchedResource unstructured.Unstructured, results []engineapi.RuleResponse) {
// check if resource and rule match
if err := e.matches(rule, policyContext, resource); err != nil {
logger.V(4).Info("rule not matched", "reason", err.Error())
@ -255,6 +255,15 @@ func (e *engine) invokeRuleHandler(
if ruleResp := e.hasPolicyExceptions(logger, ruleType, policyContext, rule); ruleResp != nil {
return resource, handlers.WithResponses(ruleResp)
}
policyContext.JSONContext().Checkpoint()
defer func() {
policyContext.JSONContext().Restore()
if patchedResource.Object != nil {
if err := policyContext.JSONContext().AddResource(patchedResource.Object); err != nil {
logger.Error(err, "failed to add resource in the json context")
}
}
}()
// load rule context
contextLoader := e.ContextLoader(policyContext.Policy(), rule)
if err := contextLoader(ctx, rule.Context, policyContext.JSONContext()); err != nil {

View file

@ -61,11 +61,13 @@ func (l *contextLoader) Load(
}
if loader != nil {
if toggle.FromContext(ctx).EnableDeferredLoading() {
if err := jsonContext.AddDeferredLoader(entry.Name, loader); err != nil {
if err := jsonContext.AddDeferredLoader(loader); err != nil {
return err
}
} else {
return loader.LoadData()
if err := loader.LoadData(); err != nil {
return err
}
}
}
}
@ -79,11 +81,11 @@ func (l *contextLoader) newLoader(
rclientFactory engineapi.RegistryClientFactory,
entry kyvernov1.ContextEntry,
jsonContext enginecontext.Interface,
) (enginecontext.Loader, error) {
) (enginecontext.DeferredLoader, error) {
if entry.ConfigMap != nil {
if l.cmResolver != nil {
l := loaders.NewConfigMapLoader(ctx, l.logger, entry, l.cmResolver, jsonContext)
return l, nil
return enginecontext.NewDeferredLoader(entry.Name, l)
} else {
l.logger.Info("disabled loading of ConfigMap context entry %s", entry.Name)
return nil, nil
@ -91,7 +93,7 @@ func (l *contextLoader) newLoader(
} else if entry.APICall != nil {
if client != nil {
l := loaders.NewAPILoader(ctx, l.logger, entry, jsonContext, jp, client)
return l, nil
return enginecontext.NewDeferredLoader(entry.Name, l)
} else {
l.logger.Info("disabled loading of APICall context entry %s", entry.Name)
return nil, nil
@ -99,14 +101,14 @@ func (l *contextLoader) newLoader(
} else if entry.ImageRegistry != nil {
if rclientFactory != nil {
l := loaders.NewImageDataLoader(ctx, l.logger, entry, jsonContext, jp, rclientFactory)
return l, nil
return enginecontext.NewDeferredLoader(entry.Name, l)
} else {
l.logger.Info("disabled loading of ImageRegistry context entry %s", entry.Name)
return nil, nil
}
} else if entry.Variable != nil {
l := loaders.NewVariableLoader(l.logger, entry, jsonContext, jp)
return l, nil
return enginecontext.NewDeferredLoader(entry.Name, l)
}
return nil, fmt.Errorf("missing ConfigMap|APICall|ImageRegistry|Variable in context entry %s", entry.Name)
}

View file

@ -269,6 +269,7 @@ func Test_variableSubstitutionCLI(t *testing.T) {
policyContext,
ctxLoaderFactory,
)
require.Equal(t, 1, len(er.PolicyResponse.Rules))
patched := er.PatchedResource

View file

@ -232,7 +232,7 @@ func NewPolicyContextFromAdmissionRequest(
gvk schema.GroupVersionKind,
configuration config.Configuration,
) (*PolicyContext, error) {
ctx, err := newVariablesContext(jp, request, &admissionInfo)
engineCtx, err := newJsonContext(jp, request, &admissionInfo)
if err != nil {
return nil, fmt.Errorf("failed to create policy rule context: %w", err)
}
@ -240,10 +240,10 @@ func NewPolicyContextFromAdmissionRequest(
if err != nil {
return nil, fmt.Errorf("failed to parse resource: %w", err)
}
if err := ctx.AddImageInfos(&newResource, configuration); err != nil {
if err := engineCtx.AddImageInfos(&newResource, configuration); err != nil {
return nil, fmt.Errorf("failed to add image information to the policy rule context: %w", err)
}
policyContext := newPolicyContextWithJsonContext(kyvernov1.AdmissionOperation(request.Operation), ctx).
policyContext := newPolicyContextWithJsonContext(kyvernov1.AdmissionOperation(request.Operation), engineCtx).
WithNewResource(newResource).
WithOldResource(oldResource).
WithAdmissionInfo(admissionInfo).
@ -253,20 +253,20 @@ func NewPolicyContextFromAdmissionRequest(
return policyContext, nil
}
func newVariablesContext(
func newJsonContext(
jp jmespath.Interface,
request admissionv1.AdmissionRequest,
userRequestInfo *kyvernov1beta1.RequestInfo,
) (enginectx.Interface, error) {
ctx := enginectx.NewContext(jp)
if err := ctx.AddRequest(request); err != nil {
engineCtx := enginectx.NewContext(jp)
if err := engineCtx.AddRequest(request); err != nil {
return nil, fmt.Errorf("failed to load incoming request in context: %w", err)
}
if err := ctx.AddUserInfo(*userRequestInfo); err != nil {
if err := engineCtx.AddUserInfo(*userRequestInfo); err != nil {
return nil, fmt.Errorf("failed to load userInfo in context: %w", err)
}
if err := ctx.AddServiceAccount(userRequestInfo.AdmissionUserInfo.Username); err != nil {
if err := engineCtx.AddServiceAccount(userRequestInfo.AdmissionUserInfo.Username); err != nil {
return nil, fmt.Errorf("failed to load service account in context: %w", err)
}
return ctx, nil
return engineCtx, nil
}

View file

@ -6,6 +6,7 @@ import (
"strings"
"testing"
kyverno "github.com/kyverno/kyverno/api/kyverno/v1"
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
kyvernov1beta1 "github.com/kyverno/kyverno/api/kyverno/v1beta1"
"github.com/kyverno/kyverno/pkg/config"
@ -50,7 +51,7 @@ func testValidate(
func newPolicyContext(
t *testing.T,
resource unstructured.Unstructured,
operation kyvernov1.AdmissionOperation,
operation kyverno.AdmissionOperation,
admissionInfo *kyvernov1beta1.RequestInfo,
) *PolicyContext {
t.Helper()
@ -2142,7 +2143,7 @@ func executeTest(t *testing.T, test testCase) {
t.Fatal(err)
}
pc := newPolicyContext(t, newR, kyvernov1.AdmissionOperation(request.Operation), &userInfo).
pc := newPolicyContext(t, newR, kyverno.AdmissionOperation(request.Operation), &userInfo).
WithPolicy(&policy).
WithOldResource(oldR)

View file

@ -0,0 +1,6 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
apply:
- manifests.yaml
assert:
- policy-assert.yaml

View file

@ -0,0 +1,5 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
apply:
- file: deploy.yaml
shouldFail: true

View file

@ -0,0 +1,12 @@
## Description
This test checks for handling of variable dependencies with deferred lookups
## Expected Behavior
The deployment should fail
## Reference Issues
https://github.com/kyverno/kyverno/issues/7486

View file

@ -0,0 +1,28 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: test
namespace: acme-fitness
labels:
app: kubecost-cost-analyzer
spec:
replicas: 3
selector:
matchLabels:
app: kubecost-cost-analyzer
template:
metadata:
labels:
app: kubecost-cost-analyzer
spec:
containers:
- name: cost-model
image: nginx:1.14.2
resources:
requests:
cpu: 350m
memory: 500Mi
limits:
memory: 2Gi

View file

@ -0,0 +1,73 @@
---
apiVersion: v1
kind: Namespace
metadata:
name: acme-fitness
---
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: enforce-company-budget
spec:
validationFailureAction: Enforce
rules:
- name: check-kubecost-budget
match:
any:
- resources:
kinds:
- Deployment
operations:
- CREATE
context:
# Mocked response from the Kubecost prediction API until it natively supports JSON input.
# Get the predicted amount of the Deployment and transform to get the totalMonthlyRate.
- name: predictedcost
variable:
jmesPath: '[0].costChange.totalMonthlyRate'
value:
- namespace: acme-fitness
controllerKind: deployment
controllerName: test
costBefore:
totalMonthlyRate: 0
cpuMonthlyRate: 0
ramMonthlyRate: 0
gpuMonthlyRate: 0
monthlyCPUCoreHours: 0
monthlyRAMByteHours: 0
monthlyGPUHours: 0
costAfter:
totalMonthlyRate: 28.839483652409793
cpuMonthlyRate: 24.295976357646456
ramMonthlyRate: 4.543507294763337
gpuMonthlyRate: 0
monthlyCPUCoreHours: 766.5
monthlyRAMByteHours: 1.14819072e+12
monthlyGPUHours: 0
costChange:
totalMonthlyRate: 92.839483652409793
cpuMonthlyRate: 24.295976357646456
ramMonthlyRate: 4.543507294763337
gpuMonthlyRate: 0
monthlyCPUCoreHours: 766.5
monthlyRAMByteHours: 1.14819072e+12
monthlyGPUHours: 0
- name: budget
variable:
value:
spendLimit: 100.0
currentSpend: 73.0
# Calculate the budget that remains from the window by subtracting the currentSpend from the spendLimit.
- name: remainingbudget
variable:
jmesPath: subtract(`{{budget.spendLimit}}`,`{{budget.currentSpend}}`)
validate:
# Need to improve this by rounding.
message: "This Deployment, which costs ${{ predictedcost }} to run for a month, will overrun the remaining budget of ${{ remainingbudget }}. Please seek approval."
deny:
conditions:
all:
- key: "{{ predictedcost }}"
operator: GreaterThan
value: "{{ remainingbudget }}"

View file

@ -0,0 +1,9 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: enforce-company-budget
status:
conditions:
- reason: Succeeded
status: "True"
type: Ready

View file

@ -0,0 +1,6 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
apply:
- manifests.yaml
assert:
- policy-assert.yaml

View file

@ -0,0 +1,7 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
apply:
- file: cm.yaml
shouldFail: false
assert:
- cm-assert.yaml

View file

@ -0,0 +1,11 @@
## Description
This test checks for deferred variable substitutions in foreach loops
## Expected Behavior
The CM should be created with three new entries
## Reference Issues
https://github.com/kyverno/kyverno/issues/7532

View file

@ -0,0 +1,8 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: testcase-7fki3-resource
data:
from_loop_1: AAA
from_loop_2: AAA
from_loop_3: AAA

View file

@ -0,0 +1,4 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: testcase-7fki3-resource

View file

@ -0,0 +1,45 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: testcase-7fki3
spec:
schemaValidation: false
background: false
validationFailureAction: Enforce
rules:
- name: mutate1
match:
all:
- resources:
kinds:
- v1/ConfigMap
names:
- testcase-7fki3-resource
context:
- name: var1
variable:
value: AAA
preconditions:
all:
- key: "{{ request.operation }}"
operator: In
value:
- CREATE
- UPDATE
mutate:
foreach:
# first loop
- list: "['dummy']"
patchStrategicMerge:
data:
from_loop_1: "{{ var1 || '!!!variable not resolved!!!' }}"
# second loop
- list: "['dummy']"
patchStrategicMerge:
data:
from_loop_2: "{{ var1 || '!!!variable not resolved!!!' }}"
# third loop
- list: "['dummy']"
patchStrategicMerge:
data:
from_loop_3: "{{ var1 || '!!!variable not resolved!!!' }}"

View file

@ -0,0 +1,9 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: testcase-7fki3
status:
conditions:
- reason: Succeeded
status: "True"
type: Ready

View file

@ -0,0 +1,6 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
apply:
- policy.yaml
assert:
- policy-assert.yaml

View file

@ -0,0 +1,6 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
apply:
- file: resource.yaml
assert:
- resource-assert.yaml

View file

@ -0,0 +1,7 @@
## Description
This test checks for handling of variable dependencies with the same name with deferred lookups in a foreach
## Expected Behavior
The configmap should create fine and contain `one: one` in the data.

View file

@ -0,0 +1,9 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: one
status:
conditions:
- reason: Succeeded
status: "True"
type: Ready

View file

@ -0,0 +1,26 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: one
spec:
validationFailureAction: Enforce
rules:
- name: one
match:
all:
- resources:
kinds:
- v1/ConfigMap
context:
- name: one
variable:
value: one
- name: one
variable:
jmesPath: one
mutate:
foreach:
- list: "['dummy']"
patchStrategicMerge:
data:
one: "{{ one }}"

View file

@ -0,0 +1,6 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: one
data:
one: one

View file

@ -0,0 +1,4 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: one

View file

@ -0,0 +1,6 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
apply:
- policy.yaml
assert:
- policy-assert.yaml

View file

@ -0,0 +1,6 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
apply:
- file: resource.yaml
assert:
- resource-assert.yaml

View file

@ -0,0 +1,9 @@
## Description
This test checks for handling of variable dependencies with the same name:
- the same name is used twice in the rule context
- the same name is also used in a foreach context
## Expected Behavior
The configmap should create fine and contain `one: one` in the data.

View file

@ -0,0 +1,9 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: one
status:
conditions:
- reason: Succeeded
status: "True"
type: Ready

View file

@ -0,0 +1,33 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: one
spec:
validationFailureAction: Enforce
rules:
- name: one
match:
all:
- resources:
kinds:
- v1/ConfigMap
context:
- name: foo
variable:
value: foo
- name: one
variable:
jmesPath: foo
- name: foo
variable:
value: baz
mutate:
foreach:
- list: "['dummy']"
context:
- name: foo
variable:
value: bar
patchStrategicMerge:
data:
one: "{{ one }}"

View file

@ -0,0 +1,6 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: one
data:
one: foo

View file

@ -0,0 +1,4 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: one

View file

@ -0,0 +1,6 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
apply:
- policy.yaml
assert:
- policy-assert.yaml

View file

@ -0,0 +1,6 @@
apiVersion: kuttl.dev/v1beta1
kind: TestStep
apply:
- file: resource.yaml
assert:
- resource-assert.yaml

View file

@ -0,0 +1,13 @@
## Description
This test checks that variables don't leak from one rule to the next.
The second rule tries to use a variable from the first rule, it should not find it.
## Expected Behavior
The configmap creates fine with the data:
```yaml
data:
one: test
two: "null"
```

View file

@ -0,0 +1,9 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: one
status:
conditions:
- reason: Succeeded
status: "True"
type: Ready

View file

@ -0,0 +1,35 @@
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: one
spec:
validationFailureAction: Enforce
rules:
- name: one
match:
all:
- resources:
kinds:
- v1/ConfigMap
context:
- name: var
variable:
value: test
mutate:
foreach:
- list: "['dummy']"
patchStrategicMerge:
data:
one: "{{ to_string(var) }}"
- name: two
match:
all:
- resources:
kinds:
- v1/ConfigMap
mutate:
foreach:
- list: "['dummy']"
patchStrategicMerge:
data:
two: "{{ to_string(var) }}"

View file

@ -0,0 +1,7 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: one
data:
one: test
two: "null"

View file

@ -0,0 +1,4 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: one