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:
parent
df5f54198d
commit
b98c0775f2
49 changed files with 1165 additions and 71 deletions
1
.github/workflows/conformance.yaml
vendored
1
.github/workflows/conformance.yaml
vendored
|
@ -58,6 +58,7 @@ jobs:
|
|||
tests:
|
||||
- autogen
|
||||
- cleanup
|
||||
- deferred
|
||||
- events
|
||||
- exceptions
|
||||
- generate/clusterpolicy
|
||||
|
|
2
Makefile
2
Makefile
|
@ -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)
|
||||
|
|
|
@ -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()),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
177
pkg/engine/context/deferred.go
Normal file
177
pkg/engine/context/deferred.go
Normal 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
|
||||
}
|
443
pkg/engine/context/deferred_test.go
Normal file
443
pkg/engine/context/deferred_test.go
Normal 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)
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -269,6 +269,7 @@ func Test_variableSubstitutionCLI(t *testing.T) {
|
|||
policyContext,
|
||||
ctxLoaderFactory,
|
||||
)
|
||||
|
||||
require.Equal(t, 1, len(er.PolicyResponse.Rules))
|
||||
|
||||
patched := er.PatchedResource
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
apiVersion: kuttl.dev/v1beta1
|
||||
kind: TestStep
|
||||
apply:
|
||||
- manifests.yaml
|
||||
assert:
|
||||
- policy-assert.yaml
|
|
@ -0,0 +1,5 @@
|
|||
apiVersion: kuttl.dev/v1beta1
|
||||
kind: TestStep
|
||||
apply:
|
||||
- file: deploy.yaml
|
||||
shouldFail: true
|
12
test/conformance/kuttl/deferred/dependencies/README.md
Normal file
12
test/conformance/kuttl/deferred/dependencies/README.md
Normal 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
|
||||
|
28
test/conformance/kuttl/deferred/dependencies/deploy.yaml
Normal file
28
test/conformance/kuttl/deferred/dependencies/deploy.yaml
Normal 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
|
73
test/conformance/kuttl/deferred/dependencies/manifests.yaml
Normal file
73
test/conformance/kuttl/deferred/dependencies/manifests.yaml
Normal 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 }}"
|
|
@ -0,0 +1,9 @@
|
|||
apiVersion: kyverno.io/v1
|
||||
kind: ClusterPolicy
|
||||
metadata:
|
||||
name: enforce-company-budget
|
||||
status:
|
||||
conditions:
|
||||
- reason: Succeeded
|
||||
status: "True"
|
||||
type: Ready
|
6
test/conformance/kuttl/deferred/foreach/01-apply.yaml
Normal file
6
test/conformance/kuttl/deferred/foreach/01-apply.yaml
Normal file
|
@ -0,0 +1,6 @@
|
|||
apiVersion: kuttl.dev/v1beta1
|
||||
kind: TestStep
|
||||
apply:
|
||||
- manifests.yaml
|
||||
assert:
|
||||
- policy-assert.yaml
|
7
test/conformance/kuttl/deferred/foreach/02-testcase.yaml
Normal file
7
test/conformance/kuttl/deferred/foreach/02-testcase.yaml
Normal file
|
@ -0,0 +1,7 @@
|
|||
apiVersion: kuttl.dev/v1beta1
|
||||
kind: TestStep
|
||||
apply:
|
||||
- file: cm.yaml
|
||||
shouldFail: false
|
||||
assert:
|
||||
- cm-assert.yaml
|
11
test/conformance/kuttl/deferred/foreach/README.md
Normal file
11
test/conformance/kuttl/deferred/foreach/README.md
Normal 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
|
8
test/conformance/kuttl/deferred/foreach/cm-assert.yaml
Normal file
8
test/conformance/kuttl/deferred/foreach/cm-assert.yaml
Normal 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
|
4
test/conformance/kuttl/deferred/foreach/cm.yaml
Normal file
4
test/conformance/kuttl/deferred/foreach/cm.yaml
Normal file
|
@ -0,0 +1,4 @@
|
|||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: testcase-7fki3-resource
|
45
test/conformance/kuttl/deferred/foreach/manifests.yaml
Normal file
45
test/conformance/kuttl/deferred/foreach/manifests.yaml
Normal 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!!!' }}"
|
|
@ -0,0 +1,9 @@
|
|||
apiVersion: kyverno.io/v1
|
||||
kind: ClusterPolicy
|
||||
metadata:
|
||||
name: testcase-7fki3
|
||||
status:
|
||||
conditions:
|
||||
- reason: Succeeded
|
||||
status: "True"
|
||||
type: Ready
|
6
test/conformance/kuttl/deferred/recursive/01-policy.yaml
Normal file
6
test/conformance/kuttl/deferred/recursive/01-policy.yaml
Normal file
|
@ -0,0 +1,6 @@
|
|||
apiVersion: kuttl.dev/v1beta1
|
||||
kind: TestStep
|
||||
apply:
|
||||
- policy.yaml
|
||||
assert:
|
||||
- policy-assert.yaml
|
|
@ -0,0 +1,6 @@
|
|||
apiVersion: kuttl.dev/v1beta1
|
||||
kind: TestStep
|
||||
apply:
|
||||
- file: resource.yaml
|
||||
assert:
|
||||
- resource-assert.yaml
|
7
test/conformance/kuttl/deferred/recursive/README.md
Normal file
7
test/conformance/kuttl/deferred/recursive/README.md
Normal 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.
|
|
@ -0,0 +1,9 @@
|
|||
apiVersion: kyverno.io/v1
|
||||
kind: ClusterPolicy
|
||||
metadata:
|
||||
name: one
|
||||
status:
|
||||
conditions:
|
||||
- reason: Succeeded
|
||||
status: "True"
|
||||
type: Ready
|
26
test/conformance/kuttl/deferred/recursive/policy.yaml
Normal file
26
test/conformance/kuttl/deferred/recursive/policy.yaml
Normal 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 }}"
|
|
@ -0,0 +1,6 @@
|
|||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: one
|
||||
data:
|
||||
one: one
|
4
test/conformance/kuttl/deferred/recursive/resource.yaml
Normal file
4
test/conformance/kuttl/deferred/recursive/resource.yaml
Normal file
|
@ -0,0 +1,4 @@
|
|||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: one
|
|
@ -0,0 +1,6 @@
|
|||
apiVersion: kuttl.dev/v1beta1
|
||||
kind: TestStep
|
||||
apply:
|
||||
- policy.yaml
|
||||
assert:
|
||||
- policy-assert.yaml
|
|
@ -0,0 +1,6 @@
|
|||
apiVersion: kuttl.dev/v1beta1
|
||||
kind: TestStep
|
||||
apply:
|
||||
- file: resource.yaml
|
||||
assert:
|
||||
- resource-assert.yaml
|
|
@ -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.
|
|
@ -0,0 +1,9 @@
|
|||
apiVersion: kyverno.io/v1
|
||||
kind: ClusterPolicy
|
||||
metadata:
|
||||
name: one
|
||||
status:
|
||||
conditions:
|
||||
- reason: Succeeded
|
||||
status: "True"
|
||||
type: Ready
|
|
@ -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 }}"
|
|
@ -0,0 +1,6 @@
|
|||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: one
|
||||
data:
|
||||
one: foo
|
|
@ -0,0 +1,4 @@
|
|||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: one
|
6
test/conformance/kuttl/deferred/two-rules/01-policy.yaml
Normal file
6
test/conformance/kuttl/deferred/two-rules/01-policy.yaml
Normal file
|
@ -0,0 +1,6 @@
|
|||
apiVersion: kuttl.dev/v1beta1
|
||||
kind: TestStep
|
||||
apply:
|
||||
- policy.yaml
|
||||
assert:
|
||||
- policy-assert.yaml
|
|
@ -0,0 +1,6 @@
|
|||
apiVersion: kuttl.dev/v1beta1
|
||||
kind: TestStep
|
||||
apply:
|
||||
- file: resource.yaml
|
||||
assert:
|
||||
- resource-assert.yaml
|
13
test/conformance/kuttl/deferred/two-rules/README.md
Normal file
13
test/conformance/kuttl/deferred/two-rules/README.md
Normal 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"
|
||||
```
|
|
@ -0,0 +1,9 @@
|
|||
apiVersion: kyverno.io/v1
|
||||
kind: ClusterPolicy
|
||||
metadata:
|
||||
name: one
|
||||
status:
|
||||
conditions:
|
||||
- reason: Succeeded
|
||||
status: "True"
|
||||
type: Ready
|
35
test/conformance/kuttl/deferred/two-rules/policy.yaml
Normal file
35
test/conformance/kuttl/deferred/two-rules/policy.yaml
Normal 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) }}"
|
|
@ -0,0 +1,7 @@
|
|||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: one
|
||||
data:
|
||||
one: test
|
||||
two: "null"
|
4
test/conformance/kuttl/deferred/two-rules/resource.yaml
Normal file
4
test/conformance/kuttl/deferred/two-rules/resource.yaml
Normal file
|
@ -0,0 +1,4 @@
|
|||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: one
|
Loading…
Reference in a new issue