diff --git a/.github/workflows/conformance.yaml b/.github/workflows/conformance.yaml index 58fee1aab8..d598390ca9 100644 --- a/.github/workflows/conformance.yaml +++ b/.github/workflows/conformance.yaml @@ -58,6 +58,7 @@ jobs: tests: - autogen - cleanup + - deferred - events - exceptions - generate/clusterpolicy diff --git a/Makefile b/Makefile index d649a9b36a..5b207902ff 100644 --- a/Makefile +++ b/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) diff --git a/cmd/cleanup-controller/handlers/cleanup/handlers.go b/cmd/cleanup-controller/handlers/cleanup/handlers.go index 7400e8f14d..eb4f0d38b0 100644 --- a/cmd/cleanup-controller/handlers/cleanup/handlers.go +++ b/cmd/cleanup-controller/handlers/cleanup/handlers.go @@ -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()), diff --git a/pkg/config/config.go b/pkg/config/config.go index aa728ea9ce..c0936fe04d 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -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 diff --git a/pkg/engine/api/resolver_test.go b/pkg/engine/api/resolver_test.go index f9e99f36b1..2647326c26 100644 --- a/pkg/engine/api/resolver_test.go +++ b/pkg/engine/api/resolver_test.go @@ -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) } diff --git a/pkg/engine/context/context.go b/pkg/engine/context/context.go index d4746d976b..92b64abbf8 100644 --- a/pkg/engine/context/context.go +++ b/pkg/engine/context/context.go @@ -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 } diff --git a/pkg/engine/context/deferred.go b/pkg/engine/context/deferred.go new file mode 100644 index 0000000000..6545b21128 --- /dev/null +++ b/pkg/engine/context/deferred.go @@ -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 +} diff --git a/pkg/engine/context/deferred_test.go b/pkg/engine/context/deferred_test.go new file mode 100644 index 0000000000..b9efd2cfa9 --- /dev/null +++ b/pkg/engine/context/deferred_test.go @@ -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, "onetwo", "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) +} diff --git a/pkg/engine/context/evaluate.go b/pkg/engine/context/evaluate.go index 78c58895f4..1624ea1b59 100644 --- a/pkg/engine/context/evaluate.go +++ b/pkg/engine/context/evaluate.go @@ -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) { diff --git a/pkg/engine/context/loader.go b/pkg/engine/context/loader.go index 8e469cf137..aed146f193 100644 --- a/pkg/engine/context/loader.go +++ b/pkg/engine/context/loader.go @@ -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) +} diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index 403e51cbf1..6449a46c37 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -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 { diff --git a/pkg/engine/factories/contextloaderfactory.go b/pkg/engine/factories/contextloaderfactory.go index a1871a311d..b288c8225f 100644 --- a/pkg/engine/factories/contextloaderfactory.go +++ b/pkg/engine/factories/contextloaderfactory.go @@ -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) } diff --git a/pkg/engine/mutation_test.go b/pkg/engine/mutation_test.go index 2266c1f15b..c0473f378d 100644 --- a/pkg/engine/mutation_test.go +++ b/pkg/engine/mutation_test.go @@ -269,6 +269,7 @@ func Test_variableSubstitutionCLI(t *testing.T) { policyContext, ctxLoaderFactory, ) + require.Equal(t, 1, len(er.PolicyResponse.Rules)) patched := er.PatchedResource diff --git a/pkg/engine/policycontext/policy_context.go b/pkg/engine/policycontext/policy_context.go index b894716684..7986294cfb 100644 --- a/pkg/engine/policycontext/policy_context.go +++ b/pkg/engine/policycontext/policy_context.go @@ -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 } diff --git a/pkg/engine/validation_test.go b/pkg/engine/validation_test.go index 00b32dbbe9..c280cf1095 100644 --- a/pkg/engine/validation_test.go +++ b/pkg/engine/validation_test.go @@ -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) diff --git a/test/conformance/kuttl/deferred/dependencies/01-apply-manifests.yaml b/test/conformance/kuttl/deferred/dependencies/01-apply-manifests.yaml new file mode 100644 index 0000000000..9ad278051c --- /dev/null +++ b/test/conformance/kuttl/deferred/dependencies/01-apply-manifests.yaml @@ -0,0 +1,6 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +apply: +- manifests.yaml +assert: +- policy-assert.yaml \ No newline at end of file diff --git a/test/conformance/kuttl/deferred/dependencies/02-testcase.yaml b/test/conformance/kuttl/deferred/dependencies/02-testcase.yaml new file mode 100644 index 0000000000..6236f803a5 --- /dev/null +++ b/test/conformance/kuttl/deferred/dependencies/02-testcase.yaml @@ -0,0 +1,5 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +apply: + - file: deploy.yaml + shouldFail: true diff --git a/test/conformance/kuttl/deferred/dependencies/README.md b/test/conformance/kuttl/deferred/dependencies/README.md new file mode 100644 index 0000000000..a19b14626b --- /dev/null +++ b/test/conformance/kuttl/deferred/dependencies/README.md @@ -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 + diff --git a/test/conformance/kuttl/deferred/dependencies/deploy.yaml b/test/conformance/kuttl/deferred/dependencies/deploy.yaml new file mode 100644 index 0000000000..c03b8fa60f --- /dev/null +++ b/test/conformance/kuttl/deferred/dependencies/deploy.yaml @@ -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 diff --git a/test/conformance/kuttl/deferred/dependencies/manifests.yaml b/test/conformance/kuttl/deferred/dependencies/manifests.yaml new file mode 100644 index 0000000000..ffdbf0a9af --- /dev/null +++ b/test/conformance/kuttl/deferred/dependencies/manifests.yaml @@ -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 }}" \ No newline at end of file diff --git a/test/conformance/kuttl/deferred/dependencies/policy-assert.yaml b/test/conformance/kuttl/deferred/dependencies/policy-assert.yaml new file mode 100644 index 0000000000..8ce29958ed --- /dev/null +++ b/test/conformance/kuttl/deferred/dependencies/policy-assert.yaml @@ -0,0 +1,9 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: enforce-company-budget +status: + conditions: + - reason: Succeeded + status: "True" + type: Ready diff --git a/test/conformance/kuttl/deferred/foreach/01-apply.yaml b/test/conformance/kuttl/deferred/foreach/01-apply.yaml new file mode 100644 index 0000000000..9ad278051c --- /dev/null +++ b/test/conformance/kuttl/deferred/foreach/01-apply.yaml @@ -0,0 +1,6 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +apply: +- manifests.yaml +assert: +- policy-assert.yaml \ No newline at end of file diff --git a/test/conformance/kuttl/deferred/foreach/02-testcase.yaml b/test/conformance/kuttl/deferred/foreach/02-testcase.yaml new file mode 100644 index 0000000000..19be21932d --- /dev/null +++ b/test/conformance/kuttl/deferred/foreach/02-testcase.yaml @@ -0,0 +1,7 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +apply: + - file: cm.yaml + shouldFail: false +assert: +- cm-assert.yaml \ No newline at end of file diff --git a/test/conformance/kuttl/deferred/foreach/README.md b/test/conformance/kuttl/deferred/foreach/README.md new file mode 100644 index 0000000000..508653b40b --- /dev/null +++ b/test/conformance/kuttl/deferred/foreach/README.md @@ -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 diff --git a/test/conformance/kuttl/deferred/foreach/cm-assert.yaml b/test/conformance/kuttl/deferred/foreach/cm-assert.yaml new file mode 100644 index 0000000000..765e7b79a0 --- /dev/null +++ b/test/conformance/kuttl/deferred/foreach/cm-assert.yaml @@ -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 diff --git a/test/conformance/kuttl/deferred/foreach/cm.yaml b/test/conformance/kuttl/deferred/foreach/cm.yaml new file mode 100644 index 0000000000..dc353e80a0 --- /dev/null +++ b/test/conformance/kuttl/deferred/foreach/cm.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: testcase-7fki3-resource diff --git a/test/conformance/kuttl/deferred/foreach/manifests.yaml b/test/conformance/kuttl/deferred/foreach/manifests.yaml new file mode 100644 index 0000000000..e4341905bc --- /dev/null +++ b/test/conformance/kuttl/deferred/foreach/manifests.yaml @@ -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!!!' }}" diff --git a/test/conformance/kuttl/deferred/foreach/policy-assert.yaml b/test/conformance/kuttl/deferred/foreach/policy-assert.yaml new file mode 100644 index 0000000000..d83273fb9e --- /dev/null +++ b/test/conformance/kuttl/deferred/foreach/policy-assert.yaml @@ -0,0 +1,9 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: testcase-7fki3 +status: + conditions: + - reason: Succeeded + status: "True" + type: Ready diff --git a/test/conformance/kuttl/deferred/recursive/01-policy.yaml b/test/conformance/kuttl/deferred/recursive/01-policy.yaml new file mode 100644 index 0000000000..b20ef0bd7d --- /dev/null +++ b/test/conformance/kuttl/deferred/recursive/01-policy.yaml @@ -0,0 +1,6 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +apply: +- policy.yaml +assert: +- policy-assert.yaml \ No newline at end of file diff --git a/test/conformance/kuttl/deferred/recursive/02-resource.yaml b/test/conformance/kuttl/deferred/recursive/02-resource.yaml new file mode 100644 index 0000000000..d0d2400d67 --- /dev/null +++ b/test/conformance/kuttl/deferred/recursive/02-resource.yaml @@ -0,0 +1,6 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +apply: + - file: resource.yaml +assert: + - resource-assert.yaml diff --git a/test/conformance/kuttl/deferred/recursive/README.md b/test/conformance/kuttl/deferred/recursive/README.md new file mode 100644 index 0000000000..461794aa76 --- /dev/null +++ b/test/conformance/kuttl/deferred/recursive/README.md @@ -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. diff --git a/test/conformance/kuttl/deferred/recursive/policy-assert.yaml b/test/conformance/kuttl/deferred/recursive/policy-assert.yaml new file mode 100644 index 0000000000..6277d9899f --- /dev/null +++ b/test/conformance/kuttl/deferred/recursive/policy-assert.yaml @@ -0,0 +1,9 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: one +status: + conditions: + - reason: Succeeded + status: "True" + type: Ready diff --git a/test/conformance/kuttl/deferred/recursive/policy.yaml b/test/conformance/kuttl/deferred/recursive/policy.yaml new file mode 100644 index 0000000000..4965a30bc4 --- /dev/null +++ b/test/conformance/kuttl/deferred/recursive/policy.yaml @@ -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 }}" diff --git a/test/conformance/kuttl/deferred/recursive/resource-assert.yaml b/test/conformance/kuttl/deferred/recursive/resource-assert.yaml new file mode 100644 index 0000000000..fbe5a01ff6 --- /dev/null +++ b/test/conformance/kuttl/deferred/recursive/resource-assert.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: one +data: + one: one diff --git a/test/conformance/kuttl/deferred/recursive/resource.yaml b/test/conformance/kuttl/deferred/recursive/resource.yaml new file mode 100644 index 0000000000..1d967e6ede --- /dev/null +++ b/test/conformance/kuttl/deferred/recursive/resource.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: one diff --git a/test/conformance/kuttl/deferred/resolve-overriden-variable/01-policy.yaml b/test/conformance/kuttl/deferred/resolve-overriden-variable/01-policy.yaml new file mode 100644 index 0000000000..b20ef0bd7d --- /dev/null +++ b/test/conformance/kuttl/deferred/resolve-overriden-variable/01-policy.yaml @@ -0,0 +1,6 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +apply: +- policy.yaml +assert: +- policy-assert.yaml \ No newline at end of file diff --git a/test/conformance/kuttl/deferred/resolve-overriden-variable/02-resource.yaml b/test/conformance/kuttl/deferred/resolve-overriden-variable/02-resource.yaml new file mode 100644 index 0000000000..d0d2400d67 --- /dev/null +++ b/test/conformance/kuttl/deferred/resolve-overriden-variable/02-resource.yaml @@ -0,0 +1,6 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +apply: + - file: resource.yaml +assert: + - resource-assert.yaml diff --git a/test/conformance/kuttl/deferred/resolve-overriden-variable/README.md b/test/conformance/kuttl/deferred/resolve-overriden-variable/README.md new file mode 100644 index 0000000000..a03b65ca6f --- /dev/null +++ b/test/conformance/kuttl/deferred/resolve-overriden-variable/README.md @@ -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. diff --git a/test/conformance/kuttl/deferred/resolve-overriden-variable/policy-assert.yaml b/test/conformance/kuttl/deferred/resolve-overriden-variable/policy-assert.yaml new file mode 100644 index 0000000000..6277d9899f --- /dev/null +++ b/test/conformance/kuttl/deferred/resolve-overriden-variable/policy-assert.yaml @@ -0,0 +1,9 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: one +status: + conditions: + - reason: Succeeded + status: "True" + type: Ready diff --git a/test/conformance/kuttl/deferred/resolve-overriden-variable/policy.yaml b/test/conformance/kuttl/deferred/resolve-overriden-variable/policy.yaml new file mode 100644 index 0000000000..7737635f08 --- /dev/null +++ b/test/conformance/kuttl/deferred/resolve-overriden-variable/policy.yaml @@ -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 }}" diff --git a/test/conformance/kuttl/deferred/resolve-overriden-variable/resource-assert.yaml b/test/conformance/kuttl/deferred/resolve-overriden-variable/resource-assert.yaml new file mode 100644 index 0000000000..da586862c4 --- /dev/null +++ b/test/conformance/kuttl/deferred/resolve-overriden-variable/resource-assert.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: one +data: + one: foo diff --git a/test/conformance/kuttl/deferred/resolve-overriden-variable/resource.yaml b/test/conformance/kuttl/deferred/resolve-overriden-variable/resource.yaml new file mode 100644 index 0000000000..1d967e6ede --- /dev/null +++ b/test/conformance/kuttl/deferred/resolve-overriden-variable/resource.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: one diff --git a/test/conformance/kuttl/deferred/two-rules/01-policy.yaml b/test/conformance/kuttl/deferred/two-rules/01-policy.yaml new file mode 100644 index 0000000000..b20ef0bd7d --- /dev/null +++ b/test/conformance/kuttl/deferred/two-rules/01-policy.yaml @@ -0,0 +1,6 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +apply: +- policy.yaml +assert: +- policy-assert.yaml \ No newline at end of file diff --git a/test/conformance/kuttl/deferred/two-rules/02-resource.yaml b/test/conformance/kuttl/deferred/two-rules/02-resource.yaml new file mode 100644 index 0000000000..d0d2400d67 --- /dev/null +++ b/test/conformance/kuttl/deferred/two-rules/02-resource.yaml @@ -0,0 +1,6 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +apply: + - file: resource.yaml +assert: + - resource-assert.yaml diff --git a/test/conformance/kuttl/deferred/two-rules/README.md b/test/conformance/kuttl/deferred/two-rules/README.md new file mode 100644 index 0000000000..7e5e28d1a9 --- /dev/null +++ b/test/conformance/kuttl/deferred/two-rules/README.md @@ -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" +``` diff --git a/test/conformance/kuttl/deferred/two-rules/policy-assert.yaml b/test/conformance/kuttl/deferred/two-rules/policy-assert.yaml new file mode 100644 index 0000000000..6277d9899f --- /dev/null +++ b/test/conformance/kuttl/deferred/two-rules/policy-assert.yaml @@ -0,0 +1,9 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: one +status: + conditions: + - reason: Succeeded + status: "True" + type: Ready diff --git a/test/conformance/kuttl/deferred/two-rules/policy.yaml b/test/conformance/kuttl/deferred/two-rules/policy.yaml new file mode 100644 index 0000000000..592fbdc5d7 --- /dev/null +++ b/test/conformance/kuttl/deferred/two-rules/policy.yaml @@ -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) }}" diff --git a/test/conformance/kuttl/deferred/two-rules/resource-assert.yaml b/test/conformance/kuttl/deferred/two-rules/resource-assert.yaml new file mode 100644 index 0000000000..aa4184d5ea --- /dev/null +++ b/test/conformance/kuttl/deferred/two-rules/resource-assert.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: one +data: + one: test + two: "null" diff --git a/test/conformance/kuttl/deferred/two-rules/resource.yaml b/test/conformance/kuttl/deferred/two-rules/resource.yaml new file mode 100644 index 0000000000..1d967e6ede --- /dev/null +++ b/test/conformance/kuttl/deferred/two-rules/resource.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: one