Repository: GoogleContainerTools/kpt
Pull request #2086 (2021-05-21 23:18:46)

support both ts and go sdk formats for fn results
workaround for https://github.com/GoogleContainerTools/kpt/issues/2056

There is a discrepancy between the results definition in functions SDK and the results definition in kpt. 

This PR adds some extra work in kpt to get the name/namespace from the functions result. 

It would be better to have a consistent schema between kpt and the functions sdk. 


Patch:
diff --git a/Makefile b/Makefile
index 5e11bd51..1a1e4faa 100644
--- a/Makefile
+++ b/Makefile
@@ -16,6 +16,14 @@
 
 GOBIN := $(shell go env GOPATH)/bin
 
+# T refers to an e2e test case matcher. This enables running e2e tests
+# selectively.  For example,
+# To invoke e2e tests related to fnconfig, run:
+# make test-fn-render T=fnconfig
+# make test-fn-eval T=fnconfig
+# By default, make test-fn-render/test-fn-eval will run all tests.
+T ?= ".*"
+
 build:
 	go build -o $(GOBIN)/kpt -v .
 
@@ -70,11 +78,11 @@ test-docker: build
 # KPT_E2E_UPDATE_EXPECTED=true (if expected output to be updated)
 # target to run e2e tests for "kpt fn render" command
 test-fn-render: build
-	PATH=$(GOBIN):$(PATH) go test -v --tags=docker --run=TestFnRender ./e2e/
+	PATH=$(GOBIN):$(PATH) go test -v --tags=docker --run=TestFnRender/testdata/fn-render/$(T) ./e2e/
 
 # target to run e2e tests for "kpt fn eval" command
 test-fn-eval: build
-	PATH=$(GOBIN):$(PATH) go test -v --tags=docker --run=TestFnEval ./e2e/
+	PATH=$(GOBIN):$(PATH) go test -v --tags=docker --run=TestFnEval/testdata/fn-eval/$(T)  ./e2e/
 
 # target to flush kpt-fn cache
 flush-fn-cache:
diff --git a/go.mod b/go.mod
index 39f62fbd..9c5b47a2 100644
--- a/go.mod
+++ b/go.mod
@@ -19,7 +19,7 @@ require (
 	k8s.io/client-go v0.21.0
 	k8s.io/klog v1.0.0
 	k8s.io/kubectl v0.21.0
-	sigs.k8s.io/cli-utils v0.25.0
+	sigs.k8s.io/cli-utils v0.25.1-0.20210521231537-8200fe56434d
 	sigs.k8s.io/kustomize/kyaml v0.10.20-0.20210506224302-fcfdf6be5152
 )
 
@@ -29,4 +29,4 @@ require (
 replace (
 	k8s.io/cli-runtime v0.20.4 => k8s.io/cli-runtime v0.21.0
 	k8s.io/kubectl v0.20.4 => k8s.io/kubectl v0.21.0
-)
\ No newline at end of file
+)
diff --git a/go.sum b/go.sum
index 17ec20be..22d58a1b 100644
--- a/go.sum
+++ b/go.sum
@@ -918,8 +918,8 @@ rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8
 rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
 sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.7/go.mod h1:PHgbrJT7lCHcxMU+mDHEm+nx46H4zuuHZkDP6icnhu0=
-sigs.k8s.io/cli-utils v0.25.0 h1:uYoe/BZBJBUluU/PknsY9mgqB8jPvQV5ADp9VT9GS2w=
-sigs.k8s.io/cli-utils v0.25.0/go.mod h1:dllg30GE57wkWbvaA9lI1cpDwTKxb9NAENStrQ3fQxg=
+sigs.k8s.io/cli-utils v0.25.1-0.20210521231537-8200fe56434d h1:4cw9zsao9BLIVxgAecemGrEWTJm9blIKJlUc6ULeZuY=
+sigs.k8s.io/cli-utils v0.25.1-0.20210521231537-8200fe56434d/go.mod h1:dllg30GE57wkWbvaA9lI1cpDwTKxb9NAENStrQ3fQxg=
 sigs.k8s.io/controller-runtime v0.6.0 h1:Fzna3DY7c4BIP6KwfSlrfnj20DJ+SeMBK8HSFvOk9NM=
 sigs.k8s.io/controller-runtime v0.6.0/go.mod h1:CpYf5pdNY/B352A1TFLAS2JVSlnGQ5O2cftPHndTroo=
 sigs.k8s.io/kustomize/api v0.8.5 h1:bfCXGXDAbFbb/Jv5AhMj2BB8a5VAJuuQ5/KU69WtDjQ=
@@ -937,4 +937,4 @@ sigs.k8s.io/structured-merge-diff/v4 v4.1.0 h1:C4r9BgJ98vrKnnVCjwCSXcWjWe0NKcUQk
 sigs.k8s.io/structured-merge-diff/v4 v4.1.0/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
 sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
 sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q=
-sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
\ No newline at end of file
+sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
diff --git a/internal/cmddestroy/cmddestroy.go b/internal/cmddestroy/cmddestroy.go
index 41481e31..84962879 100644
--- a/internal/cmddestroy/cmddestroy.go
+++ b/internal/cmddestroy/cmddestroy.go
@@ -137,8 +137,6 @@ func (r *Runner) runE(c *cobra.Command, args []string) error {
 	if r.dryRun {
 		dryRunStrategy = common.DryRunClient
 	}
-	// TODO: This is terrible. Fix it.
-	r.Destroyer.DryRunStrategy = dryRunStrategy
 
 	// TODO(mortent): Figure out if we can do this differently.
 	if r.PreProcess != nil {
@@ -160,6 +158,7 @@ func runDestroy(r *Runner, inv inventory.InventoryInfo, dryRunStrategy common.Dr
 	}
 	option := &apply.DestroyerOption{
 		InventoryPolicy: r.inventoryPolicy,
+		DryRunStrategy:  dryRunStrategy,
 	}
 	ch := r.Destroyer.Run(inv, option)
 
diff --git a/internal/cmdliveinit/cmdliveinit.go b/internal/cmdliveinit/cmdliveinit.go
index c4236130..017764f5 100644
--- a/internal/cmdliveinit/cmdliveinit.go
+++ b/internal/cmdliveinit/cmdliveinit.go
@@ -142,7 +142,7 @@ func (c *ConfigureInventoryInfo) Run(ctx context.Context) error {
 
 	// Autogenerate the name if it is not provided through the flag.
 	if c.Name == "" {
-		randomSuffix := common.RandomStr(time.Now().UTC().UnixNano())
+		randomSuffix := common.RandomStr()
 		name = fmt.Sprintf("%s-%s", defaultInventoryName, randomSuffix)
 	} else {
 		name = c.Name
diff --git a/internal/cmdrender/executor.go b/internal/cmdrender/executor.go
index 25c2240d..45e1d776 100644
--- a/internal/cmdrender/executor.go
+++ b/internal/cmdrender/executor.go
@@ -310,6 +310,11 @@ func (pn *pkgNode) runPipeline(ctx context.Context, hctx *hydrationContext, inpu
 		return input, nil
 	}
 
+	// perform runtime validation for pipeline
+	if err := pn.pkg.ValidatePipeline(); err != nil {
+		return nil, err
+	}
+
 	mutatedResources, err := pn.runMutators(ctx, hctx, input)
 	if err != nil {
 		return nil, errors.E(op, pn.pkg.UniquePath, err)
diff --git a/internal/docs/generated/livedocs/docs.go b/internal/docs/generated/livedocs/docs.go
index 4a831b7b..fb81188f 100644
--- a/internal/docs/generated/livedocs/docs.go
+++ b/internal/docs/generated/livedocs/docs.go
@@ -19,6 +19,14 @@ Args:
     Using '-' as the package path will cause kpt to read resources from stdin.
 
 Flags:
+  --dry-run:
+    It true, kpt will validate the resources in the package and print which
+    resources will be applied and which resources will be pruned, but no resources
+    will be changed.
+    If the --server-side flag is true, kpt will do a server-side dry-run, otherwise
+    it will be a client-side dry-run. Note that the output will differ somewhat
+    between the two alternatives. 
+  
   --field-manager:
     Identifier for the **owner** of the fields being applied. Only usable
     when --server-side flag is specified. Default value is kubectl.
@@ -92,6 +100,10 @@ Args:
     Using '-' as the package path will cause kpt to read resources from stdin.
 
 Flags:
+  --dry-run:
+    It true, kpt will print the resources that will be removed from the cluster, 
+    but no resources will be deleted.
+  
   --inventory-policy:
     Determines how to handle overlaps between the package being currently applied
     and existing resources in the cluster. The available options are:
@@ -120,40 +132,6 @@ var DestroyExamples = `
   kpt live destroy
 `
 
-var DiffShort = `Display the diff between the local package and the live cluster resources.`
-var DiffLong = `
-  kpt live diff [PKG_PATH | -]
-
-Args:
-  PKG_PATH | -:
-    Path to the local package which should be diffed against the cluster. It must
-    contain a Kptfile with inventory information. Defaults to the current working
-    directory.
-    Using '-' as the package path will cause kpt to read resources from stdin.
-
-Environment Variables:
-  KUBECTL_EXTERNAL_DIFF:
-    Commandline diffing tool ('diff; by default) that will be used to show
-    changes.
-    
-    # Use meld to show changes
-    KPT_EXTERNAL_DIFF=meld kpt live diff
-
-Exit statuses:
-  The following exit values are returned:
-  
-    * 0: No differences were found.
-    * 1: Differences were found.
-    * >1 kpt live or diff failed with an error.
-`
-var DiffExamples = `
-  # diff the config in the current directory against the live cluster resources.
-  kpt live diff
-  
-  # specify the local diff program to use.
-  export KUBECTL_EXTERNAL_DIFF=meld; kpt live diff my-dir
-`
-
 var InitShort = `Initialize a package with the information needed for inventory tracking.`
 var InitLong = `
   kpt live init [PKG_PATH] [flags]
@@ -223,71 +201,6 @@ var MigrateExamples = `
   kpt live migrate
 `
 
-var PreviewShort = `Preview the changes apply would make to the cluster`
-var PreviewLong = `
-  kpt live preview [PKG_PATH | -] [flags]
-
-Args:
-
-  PKG_PATH | -:
-    Path to the local package for which a preview of the operations of apply
-    or destroy should be displayed. It must contain a Kptfile with inventory
-    information. Defaults to the current working directory.
-    Using '-' as the package path will cause kpt to read resources from stdin.
-
-Flags:
-
-  --destroy:
-    If true, preview deletion of all resources.
-  
-  --field-manager:
-    Identifier for the **owner** of the fields being applied. Only usable
-    when --server-side flag is specified. Default value is kubectl.
-  
-  --force-conflicts:
-    Force overwrite of field conflicts during apply due to different field
-    managers. Only usable when --server-side flag is specified.
-    Default value is false (error and failure when field managers conflict).
-  
-  --install-resource-group:
-    Install the ResourceGroup CRD into the cluster if it isn't already
-    available. Default is false.
-  
-  --inventory-policy:
-    Determines how to handle overlaps between the package being currently applied
-    and existing resources in the cluster. The available options are:
-    
-      * strict: If any of the resources already exist in the cluster, but doesn't
-        belong to the current package, it is considered an error.
-      * adopt: If a resource already exist in the cluster, but belongs to a 
-        different package, it is considered an error. Resources that doesn't belong
-        to other packages are adopted into the current package.
-        
-    The default value is ` + "`" + `strict` + "`" + `.
-  
-  --output:
-    Determines the output format for the status information. Must be one of the following:
-    
-      * events: The output will be a list of the status events as they become available.
-      * json: The output will be a list of the status events as they become available,
-        each formatted as a json object.
-      * table: The output will be presented as a table that will be updated inline
-        as the status of resources become available.
-  
-    The default value is ‘events’.
-  
-  --server-side:
-    Perform the apply operation server-side rather than client-side.
-    Default value is false (client-side).
-`
-var PreviewExamples = `
-  # preview apply for the package in the current directory. 
-  kpt live preview
-
-  # preview destroy for a package in the my-dir directory.
-  kpt live preview --destroy my-dir
-`
-
 var StatusShort = `Display shows the status for the resources in the cluster`
 var StatusLong = `
   kpt live status [PKG_PATH | -] [flags]
diff --git a/internal/errors/resolver/container.go b/internal/errors/resolver/container.go
index 6f2dedeb..f5df0987 100644
--- a/internal/errors/resolver/container.go
+++ b/internal/errors/resolver/container.go
@@ -35,7 +35,6 @@ func (*containerImageErrorResolver) Resolve(err error) (ResolvedResult, bool) {
 		return ResolvedResult{}, false
 	}
 	return ResolvedResult{
-		Message:  containerImageError.Error(),
-		ExitCode: 1,
+		Message: containerImageError.Error(),
 	}, true
 }
diff --git a/internal/errors/resolver/git.go b/internal/errors/resolver/git.go
index 4de5fdfd..877ceca6 100644
--- a/internal/errors/resolver/git.go
+++ b/internal/errors/resolver/git.go
@@ -105,7 +105,6 @@ func (*gitExecErrorResolver) Resolve(err error) (ResolvedResult, bool) {
 		msg = ExecuteTemplate(genericGitExecError, tmplArgs)
 	}
 	return ResolvedResult{
-		Message:  msg,
-		ExitCode: 1,
+		Message: msg,
 	}, true
 }
diff --git a/internal/errors/resolver/live.go b/internal/errors/resolver/live.go
index 5fb46953..94b46fec 100644
--- a/internal/errors/resolver/live.go
+++ b/internal/errors/resolver/live.go
@@ -121,8 +121,7 @@ func (*liveErrorResolver) Resolve(err error) (ResolvedResult, bool) {
 	var invExistsError *cmdliveinit.InvExistsError
 	if errors.As(err, &invExistsError) {
 		return ResolvedResult{
-			Message:  ExecuteTemplate(invInfoAlreadyExistsMsg, tmplArgs),
-			ExitCode: 1,
+			Message: ExecuteTemplate(invInfoAlreadyExistsMsg, tmplArgs),
 		}, true
 	}
 
diff --git a/internal/errors/resolver/misc.go b/internal/errors/resolver/misc.go
index ef6a2810..f21573aa 100644
--- a/internal/errors/resolver/misc.go
+++ b/internal/errors/resolver/misc.go
@@ -30,7 +30,7 @@ type alreadyHandledErrorResolver struct{}
 func (*alreadyHandledErrorResolver) Resolve(err error) (ResolvedResult, bool) {
 	kioErr := errors.UnwrapKioError(err)
 	if goerrors.Is(kioErr, errors.ErrAlreadyHandled) {
-		return ResolvedResult{ExitCode: 1}, true
+		return ResolvedResult{}, true
 	}
 	return ResolvedResult{}, false
 }
diff --git a/internal/errors/resolver/pkg.go b/internal/errors/resolver/pkg.go
index c05079be..e2f7b959 100644
--- a/internal/errors/resolver/pkg.go
+++ b/internal/errors/resolver/pkg.go
@@ -55,20 +55,17 @@ func (*pkgErrorResolver) Resolve(err error) (ResolvedResult, bool) {
 
 		if errors.Is(kptfileError, os.ErrNotExist) {
 			return ResolvedResult{
-				Message:  ExecuteTemplate(noKptfileMsg, tmplArgs),
-				ExitCode: 1,
+				Message: ExecuteTemplate(noKptfileMsg, tmplArgs),
 			}, true
 		}
 
 		return ResolvedResult{
-			Message:  ExecuteTemplate(kptfileReadErrMsg, tmplArgs),
-			ExitCode: 1,
+			Message: ExecuteTemplate(kptfileReadErrMsg, tmplArgs),
 		}, true
 	}
 	if errors.As(err, &validateError) {
 		return ResolvedResult{
-			Message:  validateError.Error(),
-			ExitCode: 1,
+			Message: validateError.Error(),
 		}, true
 	}
 
diff --git a/internal/errors/resolver/resolver.go b/internal/errors/resolver/resolver.go
index 460ac96e..4396e703 100644
--- a/internal/errors/resolver/resolver.go
+++ b/internal/errors/resolver/resolver.go
@@ -28,9 +28,14 @@ func AddErrorResolver(er ErrorResolver) {
 // the error could not be resolved.
 func ResolveError(err error) (ResolvedResult, bool) {
 	for _, resolver := range errorResolvers {
-		msg, found := resolver.Resolve(err)
+		rr, found := resolver.Resolve(err)
+		// If the exit code hasn't been set, we default it to 1. We should
+		// never return exit code 0 for errors.
+		if rr.ExitCode == 0 {
+			rr.ExitCode = 1
+		}
 		if found {
-			return msg, true
+			return rr, true
 		}
 	}
 	return ResolvedResult{}, false
diff --git a/internal/errors/resolver/update.go b/internal/errors/resolver/update.go
index 84efb11b..f5f17374 100644
--- a/internal/errors/resolver/update.go
+++ b/internal/errors/resolver/update.go
@@ -58,8 +58,7 @@ func (*updateErrorResolver) Resolve(err error) (ResolvedResult, bool) {
 
 	if msg != "" {
 		return ResolvedResult{
-			Message:  msg,
-			ExitCode: 1,
+			Message: msg,
 		}, true
 	}
 	return ResolvedResult{}, false
diff --git a/internal/fnruntime/runner.go b/internal/fnruntime/runner.go
index 86623d78..14b79361 100644
--- a/internal/fnruntime/runner.go
+++ b/internal/fnruntime/runner.go
@@ -29,7 +29,6 @@ import (
 	"github.com/GoogleContainerTools/kpt/internal/types"
 	fnresult "github.com/GoogleContainerTools/kpt/pkg/api/fnresult/v1alpha2"
 	kptfilev1alpha2 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1alpha2"
-	"sigs.k8s.io/kustomize/kyaml/fn/framework"
 	"sigs.k8s.io/kustomize/kyaml/fn/runtime/runtimeutil"
 	"sigs.k8s.io/kustomize/kyaml/kio"
 	"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
@@ -174,6 +173,54 @@ func parseStructuredResult(yml *yaml.RNode, fnResult *fnresult.Result) error {
 	if err != nil {
 		return err
 	}
+
+	return parseNameAndNamespace(yml, fnResult)
+}
+
+// parseNameAndNamespace populates name and namespace in fnResult.Result if a
+// function (e.g. using kyaml Go SDKs) gives results in a schema
+// that puts a resourceRef's name and namespace under a metadata field
+// TODO: fix upstream (https://github.com/GoogleContainerTools/kpt/issues/2091)
+func parseNameAndNamespace(yml *yaml.RNode, fnResult *fnresult.Result) error {
+	items, err := yml.Elements()
+	if err != nil {
+		return err
+	}
+
+	for i := range items {
+		if err := populateResourceRef(items[i], &fnResult.Results[i]); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func populateResourceRef(item *yaml.RNode, resultItem *fnresult.ResultItem) error {
+	r, err := item.Pipe(yaml.Lookup("resourceRef", "metadata"))
+	if err != nil {
+		return err
+	}
+	if r == nil {
+		return nil
+	}
+	nameNode, err := r.Pipe(yaml.Lookup("name"))
+	if err != nil {
+		return err
+	}
+	namespaceNode, err := r.Pipe(yaml.Lookup("namespace"))
+	if err != nil {
+		return err
+	}
+	if nameNode != nil {
+		resultItem.ResourceRef.Name = strings.TrimSpace(nameNode.MustString())
+	}
+	if namespaceNode != nil {
+		namespace := strings.TrimSpace(namespaceNode.MustString())
+		if namespace != "" && namespace != "''" {
+			resultItem.ResourceRef.Namespace = strings.TrimSpace(namespace)
+		}
+	}
 	return nil
 }
 
@@ -300,7 +347,7 @@ func (ri *multiLineFormatter) String() string {
 }
 
 // resultToString converts given structured result item to string format.
-func resultToString(result framework.ResultItem) string {
+func resultToString(result fnresult.ResultItem) string {
 	// TODO: Go SDK should implement Stringer method
 	// for framework.ResultItem. This is a temporary
 	// wrapper that will eventually be moved to Go SDK.
@@ -333,7 +380,7 @@ func resultToString(result framework.ResultItem) string {
 	return s.String()
 }
 
-func resourceRefToString(ref yaml.ResourceMeta) string {
+func resourceRefToString(ref yaml.ResourceIdentifier) string {
 	s := strings.Builder{}
 	if ref.APIVersion != "" {
 		s.WriteString(fmt.Sprintf("%s/", ref.APIVersion))
diff --git a/internal/pkg/pkg.go b/internal/pkg/pkg.go
index 24d1783b..456fb3f2 100644
--- a/internal/pkg/pkg.go
+++ b/internal/pkg/pkg.go
@@ -385,6 +385,54 @@ func (p *Pkg) LocalResources(includeMetaResources bool) (resources []*yaml.RNode
 	return resources, err
 }
 
+// Validates the package pipeline.
+func (p *Pkg) ValidatePipeline() error {
+	pl, err := p.Pipeline()
+	if err != nil {
+		return err
+	}
+
+	if pl.IsEmpty() {
+		return nil
+	}
+
+	// read all resources including function pipeline.
+	resources, err := p.LocalResources(true)
+	if err != nil {
+		return err
+	}
+
+	resourcesByPath := sets.String{}
+
+	for _, r := range resources {
+		rPath, _, err := kioutil.GetFileAnnotations(r)
+		if err != nil {
+			return fmt.Errorf("resource missing path annotation err: %w", err)
+		}
+		resourcesByPath.Insert(filepath.Clean(rPath))
+	}
+
+	for i, fn := range pl.Mutators {
+		if fn.ConfigPath != "" && !resourcesByPath.Has(filepath.Clean(fn.ConfigPath)) {
+			return &kptfilev1alpha2.ValidateError{
+				Field:  fmt.Sprintf("pipeline.%s[%d].configPath", "mutators", i),
+				Value:  fn.ConfigPath,
+				Reason: "functionConfig must exist in the current package",
+			}
+		}
+	}
+	for i, fn := range pl.Validators {
+		if fn.ConfigPath != "" && !resourcesByPath.Has(filepath.Clean(fn.ConfigPath)) {
+			return &kptfilev1alpha2.ValidateError{
+				Field:  fmt.Sprintf("pipeline.%s[%d].configPath", "validators", i),
+				Value:  fn.ConfigPath,
+				Reason: "functionConfig must exist in the current package",
+			}
+		}
+	}
+	return nil
+}
+
 // filterMetaResources filters kpt metadata files such as Kptfile, function configs.
 func filterMetaResources(input []*yaml.RNode, pl *kptfilev1alpha2.Pipeline) (output []*yaml.RNode, err error) {
 	pathsToExclude := fnConfigFilePaths(pl)
diff --git a/pkg/api/fnresult/v1alpha2/types.go b/pkg/api/fnresult/v1alpha2/types.go
index cc761ca1..8e521bda 100644
--- a/pkg/api/fnresult/v1alpha2/types.go
+++ b/pkg/api/fnresult/v1alpha2/types.go
@@ -36,7 +36,7 @@ type Result struct {
 	// ExitCode is the exit code from running the function
 	ExitCode int `yaml:"exitCode"`
 	// Results is the list of results for the function
-	Results []framework.ResultItem `yaml:"results,omitempty"`
+	Results []ResultItem `yaml:"results,omitempty"`
 }
 
 const (
@@ -73,3 +73,24 @@ func NewResultList() *ResultList {
 		Items: []Result{},
 	}
 }
+
+// ResultItem is a duplicate of framework.ResultItem, except that
+// ResourceRef uses yaml.ResourceIdentifier here, whereas framework.ResultItem
+// uses yaml.ResourceMeta. Eventually, we will need to fix it upstream.
+// TODO: https://github.com/GoogleContainerTools/kpt/issues/2091
+type ResultItem struct {
+	// Message is a human readable message
+	Message string `yaml:"message,omitempty"`
+
+	// Severity is the severity of this result
+	Severity framework.Severity `yaml:"severity,omitempty"`
+
+	// ResourceRef is a reference to a resource
+	ResourceRef yaml.ResourceIdentifier `yaml:"resourceRef,omitempty"`
+
+	// Field is a reference to the field in a resource this result refers to
+	Field framework.Field `yaml:"field,omitempty"`
+
+	// File references a file containing the resource this result refers to
+	File framework.File `yaml:"file,omitempty"`
+}
diff --git a/pkg/api/kptfile/v1alpha2/validation.go b/pkg/api/kptfile/v1alpha2/validation.go
index 93db51de..d629ae66 100644
--- a/pkg/api/kptfile/v1alpha2/validation.go
+++ b/pkg/api/kptfile/v1alpha2/validation.go
@@ -16,17 +16,13 @@ package v1alpha2
 
 import (
 	"fmt"
-	"path"
+	"path/filepath"
 	"regexp"
 	"strings"
 
 	"sigs.k8s.io/kustomize/kyaml/yaml"
 )
 
-const (
-	sourceAllSubPkgs string = "./*"
-)
-
 func (kf *KptFile) Validate() error {
 	if err := kf.Pipeline.validate(); err != nil {
 		return fmt.Errorf("invalid pipeline: %w", err)
@@ -71,7 +67,7 @@ func (f *Function) validate(fnType string, idx int) error {
 
 	var configFields []string
 	if f.ConfigPath != "" {
-		if err := validatePath(f.ConfigPath); err != nil {
+		if err := validateFnConfigPath(f.ConfigPath); err != nil {
 			return &ValidateError{
 				Field:  fmt.Sprintf("pipeline.%s[%d].configPath", fnType, idx),
 				Value:  f.ConfigPath,
@@ -147,23 +143,21 @@ func IsNodeZero(n *yaml.Node) bool {
 		n.Line == 0 && n.Column == 0
 }
 
-// validatePath validates input path and return an error if it's invalid
-func validatePath(p string) error {
-	if path.IsAbs(p) {
-		return fmt.Errorf("path is not relative")
-	}
+// validateFnConfigPath validates syntactic correctness of given functionConfig path
+// and return an error if it's invalid.
+func validateFnConfigPath(p string) error {
 	if strings.TrimSpace(p) == "" {
-		return fmt.Errorf("path cannot have only white spaces")
+		return fmt.Errorf("path must not be empty")
 	}
-	if p != sourceAllSubPkgs && strings.Contains(p, "*") {
-		return fmt.Errorf("path contains asterisk, asterisk is only allowed in './*'")
+	p = filepath.Clean(p)
+	if filepath.IsAbs(p) {
+		return fmt.Errorf("path must be relative")
 	}
-	// backslash (\\), alert bell (\a), backspace (\b), form feed (\f), vertical tab(\v) are
-	// unlikely to be in a valid path
-	for _, c := range "\\\a\b\f\v" {
-		if strings.Contains(p, string(c)) {
-			return fmt.Errorf("path cannot have character %q", c)
-		}
+	if strings.Contains(p, "..") {
+		// fn config must not live outside the package directory
+		// Allowing outside path opens up an attack vector that allows
+		// reading any YAML file on package consumer's machine.
+		return fmt.Errorf("path must not be outside the package")
 	}
 	return nil
 }
diff --git a/pkg/live/apply-crd-task.go b/pkg/live/apply-crd-task.go
index 856cfa70..bc1ccc3e 100644
--- a/pkg/live/apply-crd-task.go
+++ b/pkg/live/apply-crd-task.go
@@ -19,7 +19,9 @@ import (
 	"k8s.io/cli-runtime/pkg/resource"
 	cmdutil "k8s.io/kubectl/pkg/cmd/util"
 	"k8s.io/kubectl/pkg/util"
+	"sigs.k8s.io/cli-utils/pkg/apply/event"
 	"sigs.k8s.io/cli-utils/pkg/apply/taskrunner"
+	"sigs.k8s.io/cli-utils/pkg/object"
 )
 
 // ApplyCRDTask encapsulates information necessary to apply a
@@ -30,6 +32,18 @@ type ApplyCRDTask struct {
 	crd     *unstructured.Unstructured
 }
 
+func (a *ApplyCRDTask) Name() string {
+	return "apply-rg-crd"
+}
+
+func (a *ApplyCRDTask) Action() event.ResourceAction {
+	return event.ApplyAction
+}
+
+func (a *ApplyCRDTask) Identifiers() []object.ObjMetadata {
+	return object.UnstructuredsToObjMetas([]*unstructured.Unstructured{a.crd})
+}
+
 // NewApplyCRDTask returns a pointer to an ApplyCRDTask struct,
 // containing fields to run the task.
 func NewApplyCRDTask(factory cmdutil.Factory, crd *unstructured.Unstructured) *ApplyCRDTask {
diff --git a/pkg/live/inventoryrg.go b/pkg/live/inventoryrg.go
index f3207322..ac811a7e 100644
--- a/pkg/live/inventoryrg.go
+++ b/pkg/live/inventoryrg.go
@@ -27,7 +27,6 @@ import (
 	"k8s.io/klog"
 	cmdutil "k8s.io/kubectl/pkg/cmd/util"
 	"sigs.k8s.io/cli-utils/pkg/apply/event"
-	"sigs.k8s.io/cli-utils/pkg/apply/task"
 	"sigs.k8s.io/cli-utils/pkg/apply/taskrunner"
 	"sigs.k8s.io/cli-utils/pkg/common"
 	"sigs.k8s.io/cli-utils/pkg/inventory"
@@ -270,13 +269,8 @@ func InstallResourceGroupCRD(factory cmdutil.Factory) error {
 		// Create the tasks to apply the ResourceGroup CRD.
 		tasks := []taskrunner.Task{
 			applyRGTask,
-			taskrunner.NewWaitTask(
-				objs,
-				taskrunner.AllCurrent,
-				applyCRDTimeout),
-			&task.ResetRESTMapperTask{
-				Mapper: mapper,
-			},
+			taskrunner.NewWaitTask("wait-rg-crd", objs, taskrunner.AllCurrent,
+				applyCRDTimeout, mapper),
 		}
 		// Create the task queue channel, and send tasks in order into the channel.
 		taskQueue := make(chan taskrunner.Task, len(tasks))
diff --git a/site/book/04-using-functions/00.md b/site/book/04-using-functions/00.md
index e6d80932..f6ed9be5 100644
--- a/site/book/04-using-functions/00.md
+++ b/site/book/04-using-functions/00.md
@@ -1,5 +1,5 @@
 [Chapter 2][ch2] provided a high-level conceptual explanation of functions. We also saw examples of
 how to use `fn eval` and `fn render` to execute functions. In this chapter, we will take a closer
-look at how to run execute functions using these two approaches.
+look at how to execute functions using these two approaches.
 
 [ch2]: /book/02-concepts/03-functions
diff --git a/thirdparty/README.md b/thirdparty/README.md
index 5930988b..13a84c31 100644
--- a/thirdparty/README.md
+++ b/thirdparty/README.md
@@ -8,7 +8,7 @@ This directory contains the files that are copied from 3rd-party projects and mo
   - `runfn`: KRM function runner
 - `cmdconfig`: Files copied from [cmd/config] v0.9.9 library
   - `commands`: Command files copied from [cmd/config]
-- `cli-utils`: Files copied from [cli-utils] v0.25.0 library
+- `cli-utils`: Files copied from [cli-utils] 8200fe56434dbb4fa897912524793af3c3384828 library
   - `commands`: Command files copied from [cli-utils/cmd]
 
 # Copyright and Licenses
@@ -24,4 +24,4 @@ The modifications made in the 3rd-party files may be contributed to upstream. Th
 [kyaml]: https://github.com/kubernetes-sigs/kustomize/tree/8d72528eb5c73df80b20aae0a5e584c056879387/kyaml
 [cmd/config]: https://github.com/kubernetes-sigs/kustomize/tree/b9c36caa1c5c6ee64926021841ea441773d0767c/cmd/config
 [cli-utils]: https://github.com/kubernetes-sigs/cli-utils
-[cli-utils/cmd]: https://github.com/kubernetes-sigs/cli-utils/tree/v0.25.0
+[cli-utils/cmd]: https://github.com/kubernetes-sigs/cli-utils/tree/8200fe56434dbb4fa897912524793af3c3384828
diff --git a/thirdparty/cli-utils/printers/events/formatter.go b/thirdparty/cli-utils/printers/events/formatter.go
index 736c5518..3fc3ab17 100644
--- a/thirdparty/cli-utils/printers/events/formatter.go
+++ b/thirdparty/cli-utils/printers/events/formatter.go
@@ -8,8 +8,6 @@ import (
 	"io"
 	"strings"
 
-	"k8s.io/apimachinery/pkg/api/meta"
-	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/cli-runtime/pkg/genericclioptions"
 	"sigs.k8s.io/cli-utils/pkg/apply/event"
@@ -29,78 +27,57 @@ type formatter struct {
 	print printFunc
 }
 
-func (ef *formatter) FormatApplyEvent(ae event.ApplyEvent, as *list.ApplyStats, c list.Collector) error {
-	switch ae.Type {
-	case event.ApplyEventCompleted:
-		output := fmt.Sprintf("%d resource(s) applied. %d created, %d unchanged, %d configured, %d failed",
-			as.Sum(), as.Created, as.Unchanged, as.Configured, as.Failed)
-		// Only print information about serverside apply if some of the
-		// resources actually were applied serverside.
-		if as.ServersideApplied > 0 {
-			output += fmt.Sprintf(", %d serverside applied", as.ServersideApplied)
-		}
-		ef.print(output)
-		for id, se := range c.LatestStatus() {
-			ef.printResourceStatus(id, se)
-		}
-	case event.ApplyEventResourceUpdate:
-		gk := ae.Identifier.GroupKind
-		name := ae.Identifier.Name
-		if ae.Error != nil {
-			ef.print("%s failed: %s", resourceIDToString(gk, name),
-				ae.Error.Error())
-		} else {
-			ef.print("%s %s", resourceIDToString(gk, name),
-				strings.ToLower(ae.Operation.String()))
-		}
+func (ef *formatter) FormatApplyEvent(ae event.ApplyEvent) error {
+	gk := ae.Identifier.GroupKind
+	name := ae.Identifier.Name
+	if ae.Error != nil {
+		ef.print("%s apply failed: %s", resourceIDToString(gk, name),
+			ae.Error.Error())
+	} else {
+		ef.print("%s %s", resourceIDToString(gk, name),
+			strings.ToLower(ae.Operation.String()))
 	}
 	return nil
 }
 
-func (ef *formatter) FormatStatusEvent(se event.StatusEvent, _ list.Collector) error {
-	if se.Type == event.StatusEventResourceUpdate {
-		id := se.Resource.Identifier
-		ef.printResourceStatus(id, se)
-	}
+func (ef *formatter) FormatStatusEvent(se event.StatusEvent) error {
+	id := se.Identifier
+	ef.printResourceStatus(id, se)
 	return nil
 }
 
-func (ef *formatter) FormatPruneEvent(pe event.PruneEvent, ps *list.PruneStats) error {
-	switch pe.Type {
-	case event.PruneEventCompleted:
-		ef.print("%d resource(s) pruned, %d skipped, %d failed", ps.Pruned, ps.Skipped, ps.Failed)
-	case event.PruneEventResourceUpdate:
-		gk := pe.Identifier.GroupKind
-		switch pe.Operation {
-		case event.Pruned:
-			ef.print("%s pruned", resourceIDToString(gk, pe.Identifier.Name))
-		case event.PruneSkipped:
-			ef.print("%s prune skipped", resourceIDToString(gk, pe.Identifier.Name))
-		}
-	case event.PruneEventFailed:
-		ef.print("%s prune failed: %s", resourceIDToString(pe.Identifier.GroupKind, pe.Identifier.Name),
+func (ef *formatter) FormatPruneEvent(pe event.PruneEvent) error {
+	gk := pe.Identifier.GroupKind
+	if pe.Error != nil {
+		ef.print("%s prune failed: %s", resourceIDToString(gk, pe.Identifier.Name),
 			pe.Error.Error())
+		return nil
+	}
+
+	switch pe.Operation {
+	case event.Pruned:
+		ef.print("%s pruned", resourceIDToString(gk, pe.Identifier.Name))
+	case event.PruneSkipped:
+		ef.print("%s prune skipped", resourceIDToString(gk, pe.Identifier.Name))
 	}
 	return nil
 }
 
-func (ef *formatter) FormatDeleteEvent(de event.DeleteEvent, ds *list.DeleteStats) error {
-	switch de.Type {
-	case event.DeleteEventCompleted:
-		ef.print("%d resource(s) deleted, %d skipped", ds.Deleted, ds.Skipped)
-	case event.DeleteEventResourceUpdate:
-		obj := de.Object
-		gvk := obj.GetObjectKind().GroupVersionKind()
-		name := getName(obj)
-		switch de.Operation {
-		case event.Deleted:
-			ef.print("%s deleted", resourceIDToString(gvk.GroupKind(), name))
-		case event.DeleteSkipped:
-			ef.print("%s delete skipped", resourceIDToString(gvk.GroupKind(), name))
-		}
-	case event.DeleteEventFailed:
-		ef.print("%s deletion failed: %s", resourceIDToString(de.Identifier.GroupKind, de.Identifier.Name),
+func (ef *formatter) FormatDeleteEvent(de event.DeleteEvent) error {
+	gk := de.Identifier.GroupKind
+	name := de.Identifier.Name
+
+	if de.Error != nil {
+		ef.print("%s deletion failed: %s", resourceIDToString(gk, name),
 			de.Error.Error())
+		return nil
+	}
+
+	switch de.Operation {
+	case event.Deleted:
+		ef.print("%s deleted", resourceIDToString(gk, name))
+	case event.DeleteSkipped:
+		ef.print("%s delete skipped", resourceIDToString(gk, name))
 	}
 	return nil
 }
@@ -109,18 +86,46 @@ func (ef *formatter) FormatErrorEvent(_ event.ErrorEvent) error {
 	return nil
 }
 
-func (ef *formatter) printResourceStatus(id object.ObjMetadata, se event.StatusEvent) {
-	ef.print("%s is %s: %s", resourceIDToString(id.GroupKind, id.Name),
-		se.Resource.Status.String(), se.Resource.Message)
-}
+func (ef *formatter) FormatActionGroupEvent(age event.ActionGroupEvent, ags []event.ActionGroup,
+	as *list.ApplyStats, ps *list.PruneStats, ds *list.DeleteStats, c list.Collector) error {
+	if age.Action == event.ApplyAction && age.Type == event.Finished {
+		output := fmt.Sprintf("%d resource(s) applied. %d created, %d unchanged, %d configured, %d failed",
+			as.Sum(), as.Created, as.Unchanged, as.Configured, as.Failed)
+		// Only print information about serverside apply if some of the
+		// resources actually were applied serverside.
+		if as.ServersideApplied > 0 {
+			output += fmt.Sprintf(", %d serverside applied", as.ServersideApplied)
+		}
+		ef.print(output)
+	}
 
-func getName(obj runtime.Object) string {
-	if acc, err := meta.Accessor(obj); err == nil {
-		if n := acc.GetName(); len(n) > 0 {
-			return n
+	if age.Action == event.PruneAction && age.Type == event.Finished {
+		ef.print("%d resource(s) pruned, %d skipped, %d failed", ps.Pruned, ps.Skipped, ps.Failed)
+	}
+
+	if age.Action == event.DeleteAction && age.Type == event.Finished {
+		ef.print("%d resource(s) deleted, %d skipped", ds.Deleted, ds.Skipped)
+	}
+
+	if age.Action == event.WaitAction && age.Type == event.Started {
+		ag, found := list.ActionGroupByName(age.GroupName, ags)
+		if !found {
+			panic(fmt.Errorf("unknown action group name %q", age.GroupName))
+		}
+		for id, se := range c.LatestStatus() {
+			// Only print information about objects that we actually care about
+			// for this wait task.
+			if found := object.ObjMetas(ag.Identifiers).Contains(id); found {
+				ef.printResourceStatus(id, se)
+			}
 		}
 	}
-	return "<unknown>"
+	return nil
+}
+
+func (ef *formatter) printResourceStatus(id object.ObjMetadata, se event.StatusEvent) {
+	ef.print("%s is %s: %s", resourceIDToString(id.GroupKind, id.Name),
+		se.PollResourceInfo.Status.String(), se.PollResourceInfo.Message)
 }
 
 // resourceIDToString returns the string representation of a GroupKind and a resource name.
diff --git a/thirdparty/cli-utils/printers/events/printer.go b/thirdparty/cli-utils/printers/events/printer.go
index 8742d12a..38c871cc 100644
--- a/thirdparty/cli-utils/printers/events/printer.go
+++ b/thirdparty/cli-utils/printers/events/printer.go
@@ -6,12 +6,14 @@ package events
 import (
 	"github.com/GoogleContainerTools/kpt/thirdparty/cli-utils/printers/printer"
 	"k8s.io/cli-runtime/pkg/genericclioptions"
+	"sigs.k8s.io/cli-utils/pkg/common"
 	"sigs.k8s.io/cli-utils/pkg/print/list"
 )
 
 func NewPrinter(ioStreams genericclioptions.IOStreams) printer.Printer {
 	return &list.BaseListPrinter{
-		IOStreams:        ioStreams,
-		FormatterFactory: NewFormatter,
+		FormatterFactory: func(previewStrategy common.DryRunStrategy) list.Formatter {
+			return NewFormatter(ioStreams, previewStrategy)
+		},
 	}
 }
diff --git a/thirdparty/cli-utils/printers/json/formatter.go b/thirdparty/cli-utils/printers/json/formatter.go
index c402654c..7d8a7b19 100644
--- a/thirdparty/cli-utils/printers/json/formatter.go
+++ b/thirdparty/cli-utils/printers/json/formatter.go
@@ -28,9 +28,56 @@ type formatter struct {
 	ioStreams      genericclioptions.IOStreams
 }
 
-func (jf *formatter) FormatApplyEvent(ae event.ApplyEvent, as *list.ApplyStats, c list.Collector) error {
-	switch ae.Type {
-	case event.ApplyEventCompleted:
+func (jf *formatter) FormatApplyEvent(ae event.ApplyEvent) error {
+	eventInfo := jf.baseResourceEvent(ae.Identifier)
+	if ae.Error != nil {
+		eventInfo["error"] = ae.Error.Error()
+		return jf.printEvent("apply", "resourceFailed", eventInfo)
+	}
+	eventInfo["operation"] = ae.Operation.String()
+	return jf.printEvent("apply", "resourceApplied", eventInfo)
+}
+
+func (jf *formatter) FormatStatusEvent(se event.StatusEvent) error {
+	return jf.printResourceStatus(se)
+}
+
+func (jf *formatter) printResourceStatus(se event.StatusEvent) error {
+	eventInfo := jf.baseResourceEvent(se.Identifier)
+	eventInfo["status"] = se.PollResourceInfo.Status.String()
+	eventInfo["message"] = se.PollResourceInfo.Message
+	return jf.printEvent("status", "resourceStatus", eventInfo)
+}
+
+func (jf *formatter) FormatPruneEvent(pe event.PruneEvent) error {
+	eventInfo := jf.baseResourceEvent(pe.Identifier)
+	if pe.Error != nil {
+		eventInfo["error"] = pe.Error.Error()
+		return jf.printEvent("prune", "resourceFailed", eventInfo)
+	}
+	eventInfo["operation"] = pe.Operation.String()
+	return jf.printEvent("prune", "resourcePruned", eventInfo)
+}
+
+func (jf *formatter) FormatDeleteEvent(de event.DeleteEvent) error {
+	eventInfo := jf.baseResourceEvent(de.Identifier)
+	if de.Error != nil {
+		eventInfo["error"] = de.Error.Error()
+		return jf.printEvent("delete", "resourceFailed", eventInfo)
+	}
+	eventInfo["operation"] = de.Operation.String()
+	return jf.printEvent("delete", "resourceDeleted", eventInfo)
+}
+
+func (jf *formatter) FormatErrorEvent(ee event.ErrorEvent) error {
+	return jf.printEvent("error", "error", map[string]interface{}{
+		"error": ee.Err.Error(),
+	})
+}
+
+func (jf *formatter) FormatActionGroupEvent(age event.ActionGroupEvent, ags []event.ActionGroup,
+	as *list.ApplyStats, ps *list.PruneStats, ds *list.DeleteStats, c list.Collector) error {
+	if age.Action == event.ApplyAction && age.Type == event.Finished {
 		if err := jf.printEvent("apply", "completed", map[string]interface{}{
 			"count":           as.Sum(),
 			"createdCount":    as.Created,
@@ -41,112 +88,47 @@ func (jf *formatter) FormatApplyEvent(ae event.ApplyEvent, as *list.ApplyStats,
 		}); err != nil {
 			return err
 		}
-
-		for id, se := range c.LatestStatus() {
-			if err := jf.printResourceStatus(id, se); err != nil {
-				return err
-			}
-		}
-	case event.ApplyEventResourceUpdate:
-		gk := ae.Identifier.GroupKind
-		eventInfo := map[string]interface{}{
-			"group":     gk.Group,
-			"kind":      gk.Kind,
-			"namespace": ae.Identifier.Namespace,
-			"name":      ae.Identifier.Name,
-			"operation": ae.Operation.String(),
-		}
-		if ae.Error != nil {
-			eventInfo["error"] = ae.Error.Error()
-		}
-
-		return jf.printEvent("apply", "resourceApplied", eventInfo)
 	}
-	return nil
-}
 
-func (jf *formatter) FormatStatusEvent(se event.StatusEvent, _ list.Collector) error {
-	if se.Type == event.StatusEventResourceUpdate {
-		id := se.Resource.Identifier
-		return jf.printResourceStatus(id, se)
-	}
-	return nil
-}
-
-func (jf *formatter) printResourceStatus(id object.ObjMetadata, se event.StatusEvent) error {
-	return jf.printEvent("status", "resourceStatus",
-		map[string]interface{}{
-			"group":     id.GroupKind.Group,
-			"kind":      id.GroupKind.Kind,
-			"namespace": id.Namespace,
-			"name":      id.Name,
-			"status":    se.Resource.Status.String(),
-			"message":   se.Resource.Message,
-		})
-}
-
-func (jf *formatter) FormatPruneEvent(pe event.PruneEvent, ps *list.PruneStats) error {
-	switch pe.Type {
-	case event.PruneEventCompleted:
+	if age.Action == event.PruneAction && age.Type == event.Finished {
 		return jf.printEvent("prune", "completed", map[string]interface{}{
 			"pruned":  ps.Pruned,
 			"skipped": ps.Skipped,
 		})
-	case event.PruneEventResourceUpdate:
-		gk := pe.Identifier.GroupKind
-		return jf.printEvent("prune", "resourcePruned", map[string]interface{}{
-			"group":     gk.Group,
-			"kind":      gk.Kind,
-			"namespace": pe.Identifier.Namespace,
-			"name":      pe.Identifier.Name,
-			"operation": pe.Operation.String(),
-		})
-	case event.PruneEventFailed:
-		gk := pe.Identifier.GroupKind
-		return jf.printEvent("prune", "resourceFailed", map[string]interface{}{
-			"group":     gk.Group,
-			"kind":      gk.Kind,
-			"namespace": pe.Identifier.Namespace,
-			"name":      pe.Identifier.Name,
-			"error":     pe.Error.Error(),
-		})
 	}
-	return nil
-}
 
-func (jf *formatter) FormatDeleteEvent(de event.DeleteEvent, ds *list.DeleteStats) error {
-	switch de.Type {
-	case event.DeleteEventCompleted:
+	if age.Action == event.DeleteAction && age.Type == event.Finished {
 		return jf.printEvent("delete", "completed", map[string]interface{}{
 			"deleted": ds.Deleted,
 			"skipped": ds.Skipped,
 		})
-	case event.DeleteEventResourceUpdate:
-		gk := de.Identifier.GroupKind
-		return jf.printEvent("delete", "resourceDeleted", map[string]interface{}{
-			"group":     gk.Group,
-			"kind":      gk.Kind,
-			"namespace": de.Identifier.Namespace,
-			"name":      de.Identifier.Name,
-			"operation": de.Operation.String(),
-		})
-	case event.DeleteEventFailed:
-		gk := de.Identifier.GroupKind
-		return jf.printEvent("delete", "resourceFailed", map[string]interface{}{
-			"group":     gk.Group,
-			"kind":      gk.Kind,
-			"namespace": de.Identifier.Namespace,
-			"name":      de.Identifier.Name,
-			"error":     de.Error.Error(),
-		})
+	}
+
+	if age.Action == event.WaitAction && age.Type == event.Started {
+		ag, found := list.ActionGroupByName(age.GroupName, ags)
+		if !found {
+			panic(fmt.Errorf("unknown action group name %q", age.GroupName))
+		}
+		for id, se := range c.LatestStatus() {
+			// Only print information about objects that we actually care about
+			// for this wait task.
+			if found := object.ObjMetas(ag.Identifiers).Contains(id); found {
+				if err := jf.printResourceStatus(se); err != nil {
+					return err
+				}
+			}
+		}
 	}
 	return nil
 }
 
-func (jf *formatter) FormatErrorEvent(ee event.ErrorEvent) error {
-	return jf.printEvent("error", "error", map[string]interface{}{
-		"error": ee.Err.Error(),
-	})
+func (jf *formatter) baseResourceEvent(identifier object.ObjMetadata) map[string]interface{} {
+	return map[string]interface{}{
+		"group":     identifier.GroupKind.Group,
+		"kind":      identifier.GroupKind.Kind,
+		"namespace": identifier.Namespace,
+		"name":      identifier.Name,
+	}
 }
 
 func (jf *formatter) printEvent(t, eventType string, content map[string]interface{}) error {
diff --git a/thirdparty/cli-utils/printers/json/printer.go b/thirdparty/cli-utils/printers/json/printer.go
index a4d57fdd..2a673a66 100644
--- a/thirdparty/cli-utils/printers/json/printer.go
+++ b/thirdparty/cli-utils/printers/json/printer.go
@@ -6,12 +6,14 @@ package json
 import (
 	"github.com/GoogleContainerTools/kpt/thirdparty/cli-utils/printers/printer"
 	"k8s.io/cli-runtime/pkg/genericclioptions"
+	"sigs.k8s.io/cli-utils/pkg/common"
 	"sigs.k8s.io/cli-utils/pkg/print/list"
 )
 
 func NewPrinter(ioStreams genericclioptions.IOStreams) printer.Printer {
 	return &list.BaseListPrinter{
-		IOStreams:        ioStreams,
-		FormatterFactory: NewFormatter,
+		FormatterFactory: func(previewStrategy common.DryRunStrategy) list.Formatter {
+			return NewFormatter(ioStreams, previewStrategy)
+		},
 	}
 }
diff --git a/thirdparty/cli-utils/printers/printers.go b/thirdparty/cli-utils/printers/printers.go
index 93ffc5e0..94fbdaac 100644
--- a/thirdparty/cli-utils/printers/printers.go
+++ b/thirdparty/cli-utils/printers/printers.go
@@ -9,7 +9,6 @@ import (
 	"github.com/GoogleContainerTools/kpt/thirdparty/cli-utils/printers/printer"
 	"github.com/GoogleContainerTools/kpt/thirdparty/cli-utils/printers/table"
 	"k8s.io/cli-runtime/pkg/genericclioptions"
-	"sigs.k8s.io/cli-utils/pkg/print/list"
 )
 
 const (
@@ -25,10 +24,7 @@ func GetPrinter(printerType string, ioStreams genericclioptions.IOStreams) print
 			IOStreams: ioStreams,
 		}
 	case JSONPrinter:
-		return &list.BaseListPrinter{
-			IOStreams:        ioStreams,
-			FormatterFactory: json.NewFormatter,
-		}
+		return json.NewPrinter(ioStreams)
 	default:
 		return events.NewPrinter(ioStreams)
 	}
diff --git a/thirdparty/cli-utils/printers/table/collector.go b/thirdparty/cli-utils/printers/table/collector.go
index 24ddcade..b4ae5d2a 100644
--- a/thirdparty/cli-utils/printers/table/collector.go
+++ b/thirdparty/cli-utils/printers/table/collector.go
@@ -15,7 +15,7 @@ import (
 	"sigs.k8s.io/cli-utils/pkg/print/table"
 )
 
-func newResourceStateCollector(resourceGroups []event.ResourceGroup) *ResourceStateCollector {
+func newResourceStateCollector(resourceGroups []event.ActionGroup) *ResourceStateCollector {
 	resourceInfos := make(map[object.ObjMetadata]*ResourceInfo)
 	for _, group := range resourceGroups {
 		action := group.Action
@@ -70,15 +70,15 @@ type ResourceInfo struct {
 
 	// ApplyOpResult contains the result after
 	// a resource has been applied to the cluster.
-	ApplyOpResult *event.ApplyEventOperation
+	ApplyOpResult event.ApplyEventOperation
 
 	// PruneOpResult contains the result after
 	// a prune operation on a resource
-	PruneOpResult *event.PruneEventOperation
+	PruneOpResult event.PruneEventOperation
 
 	// DeleteOpResult contains the result after
 	// a delete operation on a resource
-	DeleteOpResult *event.DeleteEventOperation
+	DeleteOpResult event.DeleteEventOperation
 }
 
 // Identifier returns the identifier for the given resource.
@@ -182,48 +182,37 @@ func (r *ResourceStateCollector) processEvent(ev event.Event) error {
 // processStatusEvent handles events pertaining to a status
 // update for a resource.
 func (r *ResourceStateCollector) processStatusEvent(e event.StatusEvent) {
-	if e.Type == event.StatusEventResourceUpdate {
-		klog.V(7).Infoln("processing status event")
-		resource := e.Resource
-		if resource == nil {
-			klog.V(4).Infoln("status event missing Resource field; no processing")
-			return
-		}
-		previous, found := r.resourceInfos[resource.Identifier]
-		if !found {
-			klog.V(4).Infof("%s status event not found in ResourceInfos; no processing", resource.Identifier)
-			return
-		}
-		previous.resourceStatus = e.Resource
+	klog.V(7).Infoln("processing status event")
+	previous, found := r.resourceInfos[e.Identifier]
+	if !found {
+		klog.V(4).Infof("%s status event not found in ResourceInfos; no processing", e.Identifier)
+		return
 	}
+	previous.resourceStatus = e.PollResourceInfo
 }
 
 // processApplyEvent handles events relating to apply operations
 func (r *ResourceStateCollector) processApplyEvent(e event.ApplyEvent) {
-	if e.Type == event.ApplyEventResourceUpdate {
-		identifier := e.Identifier
-		klog.V(7).Infof("processing apply event for %s", identifier)
-		previous, found := r.resourceInfos[identifier]
-		if !found {
-			klog.V(4).Infof("%s apply event not found in ResourceInfos; no processing", identifier)
-			return
-		}
-		previous.ApplyOpResult = &e.Operation
+	identifier := e.Identifier
+	klog.V(7).Infof("processing apply event for %s", identifier)
+	previous, found := r.resourceInfos[identifier]
+	if !found {
+		klog.V(4).Infof("%s apply event not found in ResourceInfos; no processing", identifier)
+		return
 	}
+	previous.ApplyOpResult = e.Operation
 }
 
 // processPruneEvent handles event related to prune operations.
 func (r *ResourceStateCollector) processPruneEvent(e event.PruneEvent) {
-	if e.Type == event.PruneEventResourceUpdate {
-		identifier := e.Identifier
-		klog.V(7).Infof("processing prune event for %s", identifier)
-		previous, found := r.resourceInfos[identifier]
-		if !found {
-			klog.V(4).Infof("%s prune event not found in ResourceInfos; no processing", identifier)
-			return
-		}
-		previous.PruneOpResult = &e.Operation
+	identifier := e.Identifier
+	klog.V(7).Infof("processing prune event for %s", identifier)
+	previous, found := r.resourceInfos[identifier]
+	if !found {
+		klog.V(4).Infof("%s prune event not found in ResourceInfos; no processing", identifier)
+		return
 	}
+	previous.PruneOpResult = e.Operation
 }
 
 // ResourceState contains the latest state for all the resources.
diff --git a/thirdparty/cli-utils/printers/table/printer.go b/thirdparty/cli-utils/printers/table/printer.go
index b303d33e..ffa412dc 100644
--- a/thirdparty/cli-utils/printers/table/printer.go
+++ b/thirdparty/cli-utils/printers/table/printer.go
@@ -35,7 +35,7 @@ func (t *Printer) Print(ch <-chan event.Event, _ common.DryRunStrategy) error {
 	}
 	// Create a new collector and initialize it with the resources
 	// we are interested in.
-	coll := newResourceStateCollector(initEvent.ResourceGroups)
+	coll := newResourceStateCollector(initEvent.ActionGroups)
 
 	stop := make(chan struct{})
 
@@ -89,11 +89,11 @@ var (
 			var text string
 			switch resInfo.ResourceAction {
 			case event.ApplyAction:
-				if resInfo.ApplyOpResult != nil {
+				if resInfo.ApplyOpResult != event.ApplyUnspecified {
 					text = resInfo.ApplyOpResult.String()
 				}
 			case event.PruneAction:
-				if resInfo.PruneOpResult != nil {
+				if resInfo.PruneOpResult != event.PruneUnspecified {
 					text = resInfo.PruneOpResult.String()
 				}
 			}
diff --git a/thirdparty/cli-utils/status/cmdstatus.go b/thirdparty/cli-utils/status/cmdstatus.go
index 216551b4..d332949a 100644
--- a/thirdparty/cli-utils/status/cmdstatus.go
+++ b/thirdparty/cli-utils/status/cmdstatus.go
@@ -57,6 +57,7 @@ func NewRunner(ctx context.Context, provider provider.Provider, loader manifestr
 		Long:    livedocs.StatusLong,
 		Example: livedocs.StatusExamples,
 	}
+	r.Command = c
 	c.Flags().DurationVar(&r.period, "poll-period", 2*time.Second,
 		"Polling period for resource statuses.")
 	c.Flags().StringVar(&r.pollUntil, "poll-until", "known",
@@ -64,8 +65,6 @@ func NewRunner(ctx context.Context, provider provider.Provider, loader manifestr
 	c.Flags().StringVar(&r.output, "output", "events", "Output format.")
 	c.Flags().DurationVar(&r.timeout, "timeout", 0,
 		"How long to wait before exiting")
-
-	r.Command = c
 	return r
 }
 
diff --git a/thirdparty/cli-utils/status/printers/table/table_printer.go b/thirdparty/cli-utils/status/printers/table/table_printer.go
index 6f248889..d838c978 100644
--- a/thirdparty/cli-utils/status/printers/table/table_printer.go
+++ b/thirdparty/cli-utils/status/printers/table/table_printer.go
@@ -33,7 +33,7 @@ func NewTablePrinter(ioStreams genericclioptions.IOStreams) *tablePrinter {
 
 // Print take an event channel and outputs the status events on the channel
 // until the channel is closed .
-// nolint
+//nolint:interfacer
 func (t *tablePrinter) Print(ch <-chan event.Event, identifiers []object.ObjMetadata,
 	cancelFunc collector.ObserverFunc) {
 	coll := collector.NewResourceStatusCollector(identifiers)


Test patch:
diff --git a/e2e/live/end-to-end-test.sh b/e2e/live/end-to-end-test.sh
index 68bc70ec..b8809938 100755
--- a/e2e/live/end-to-end-test.sh
+++ b/e2e/live/end-to-end-test.sh
@@ -632,7 +632,7 @@ assertContains "namespace: continue-err-namespace"
 ${BIN_DIR}/kpt live apply e2e/live/testdata/continue-on-error > $OUTPUT_DIR/status
 assertRGInventory "continue-err-namespace" "2"
 assertContains "pod/pod-a created"
-assertContains "pod/pod-B failed"
+assertContains "pod/pod-B apply failed"
 assertPodExists "pod-a" "continue-err-namespace"
 assertPodNotExists "pod-B" "continue-err-namespace"
 printResult
@@ -664,7 +664,7 @@ wait 2
 # in the "rbac-error" namespace (succeeds).
 ${BIN_DIR}/kpt live apply e2e/live/testdata/rbac-error-step-2 > $OUTPUT_DIR/status
 assertRGInventory "rbac-error" "1"
-assertContains "configmap/error-config-map failed"
+assertContains "configmap/error-config-map apply failed"
 assertContains "configmap/valid-config-map created"
 assertContains "2 resource(s) applied. 1 created, 0 unchanged, 0 configured, 1 failed"
 assertContains "0 resource(s) pruned, 0 skipped, 0 failed"
diff --git a/e2e/testdata/fn-eval/function-chain/.expected/exec.sh b/e2e/testdata/fn-eval/function-chain/.expected/exec.sh
index 1c7b0301..7fbfddd7 100644
--- a/e2e/testdata/fn-eval/function-chain/.expected/exec.sh
+++ b/e2e/testdata/fn-eval/function-chain/.expected/exec.sh
@@ -1,6 +1,21 @@
 #! /bin/bash
+# Copyright 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
 
-kpt fn source |\
-kpt fn eval - --image gcr.io/kpt-fn/set-namespace:v0.1 -- namespace=staging |\
-kpt fn eval - --image gcr.io/kpt-fn/set-label:v0.1 -- foo=bar |\
-kpt fn sink .
+set -eo pipefail
+
+kpt fn source \
+| kpt fn eval - --image gcr.io/kpt-fn/set-namespace:v0.1 -- namespace=staging \
+| kpt fn eval - --image gcr.io/kpt-fn/set-label:v0.1 -- foo=bar \
+| kpt fn sink .
diff --git a/e2e/testdata/fn-eval/out-of-place/.expected/diff.patch b/e2e/testdata/fn-eval/out-of-place/.expected/diff.patch
new file mode 100644
index 00000000..63ccc2b3
--- /dev/null
+++ b/e2e/testdata/fn-eval/out-of-place/.expected/diff.patch
@@ -0,0 +1,19 @@
+diff --git a/resources.yaml b/resources.yaml
+index 7a494c9..254b9cd 100644
+--- a/resources.yaml
++++ b/resources.yaml
+@@ -15,6 +15,7 @@ apiVersion: apps/v1
+ kind: Deployment
+ metadata:
+   name: nginx-deployment
++  namespace: staging
+ spec:
+   replicas: 3
+ ---
+@@ -22,5 +23,6 @@ apiVersion: custom.io/v1
+ kind: Custom
+ metadata:
+   name: custom
++  namespace: staging
+ spec:
+   image: nginx:1.2.3
diff --git a/e2e/testdata/fn-eval/out-of-place/.expected/exec.sh b/e2e/testdata/fn-eval/out-of-place/.expected/exec.sh
new file mode 100644
index 00000000..4ae6a34a
--- /dev/null
+++ b/e2e/testdata/fn-eval/out-of-place/.expected/exec.sh
@@ -0,0 +1,30 @@
+#! /bin/bash
+# Copyright 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set -eo pipefail
+
+# create a temporary directory
+TEMP_DIR=$(mktemp -d)
+
+kpt fn source \
+| kpt fn eval - --image gcr.io/kpt-fn/set-namespace:v0.1 -- namespace=staging \
+| kpt fn sink $TEMP_DIR
+
+# copy back the resources
+rm -r ./*
+cp $TEMP_DIR/* .
+
+# remove temporary directory
+rm -r $TEMP_DIR
diff --git a/e2e/testdata/fn-eval/out-of-place/.krmignore b/e2e/testdata/fn-eval/out-of-place/.krmignore
new file mode 100644
index 00000000..9d7a4007
--- /dev/null
+++ b/e2e/testdata/fn-eval/out-of-place/.krmignore
@@ -0,0 +1,1 @@
+.expected
diff --git a/e2e/testdata/fn-eval/out-of-place/resources.yaml b/e2e/testdata/fn-eval/out-of-place/resources.yaml
new file mode 100644
index 00000000..7a494c9c
--- /dev/null
+++ b/e2e/testdata/fn-eval/out-of-place/resources.yaml
@@ -0,0 +1,26 @@
+# Copyright 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: nginx-deployment
+spec:
+  replicas: 3
+---
+apiVersion: custom.io/v1
+kind: Custom
+metadata:
+  name: custom
+spec:
+  image: nginx:1.2.3
diff --git a/e2e/testdata/fn-eval/privilege-options/disable-network/.expected/config.yaml b/e2e/testdata/fn-eval/privilege-options/disable-network/.expected/config.yaml
new file mode 100644
index 00000000..3b172ec1
--- /dev/null
+++ b/e2e/testdata/fn-eval/privilege-options/disable-network/.expected/config.yaml
@@ -0,0 +1,15 @@
+# Copyright 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+exitCode: 1
diff --git a/e2e/testdata/fn-eval/privilege-options/disable-network/.expected/exec.sh b/e2e/testdata/fn-eval/privilege-options/disable-network/.expected/exec.sh
new file mode 100644
index 00000000..54cca89b
--- /dev/null
+++ b/e2e/testdata/fn-eval/privilege-options/disable-network/.expected/exec.sh
@@ -0,0 +1,21 @@
+#! /bin/bash
+# Copyright 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set -eo pipefail
+
+kpt fn eval \
+--image gcr.io/kpt-fn/kubeval:v0.1 \
+-- \
+schema_location='https://kubernetesjsonschema.dev'
diff --git a/e2e/testdata/fn-eval/privilege-options/disable-network/.krmginore b/e2e/testdata/fn-eval/privilege-options/disable-network/.krmginore
new file mode 100644
index 00000000..9d7a4007
--- /dev/null
+++ b/e2e/testdata/fn-eval/privilege-options/disable-network/.krmginore
@@ -0,0 +1,1 @@
+.expected
diff --git a/e2e/testdata/fn-eval/privilege-options/disable-network/resources.yaml b/e2e/testdata/fn-eval/privilege-options/disable-network/resources.yaml
new file mode 100644
index 00000000..502aebb0
--- /dev/null
+++ b/e2e/testdata/fn-eval/privilege-options/disable-network/resources.yaml
@@ -0,0 +1,21 @@
+# Copyright 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: some-secret
+  namespace: default
+data:
+  less_sensitive_key: less sensitive data goes here
+  private_key: sensitive data goes here
diff --git a/e2e/testdata/fn-eval/privilege-options/enable-network/.expected/exec.sh b/e2e/testdata/fn-eval/privilege-options/enable-network/.expected/exec.sh
new file mode 100644
index 00000000..934d7f8b
--- /dev/null
+++ b/e2e/testdata/fn-eval/privilege-options/enable-network/.expected/exec.sh
@@ -0,0 +1,22 @@
+#! /bin/bash
+# Copyright 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set -eo pipefail
+
+kpt fn eval \
+--image gcr.io/kpt-fn/kubeval:v0.1 \
+--network \
+-- \
+schema_location='https://kubernetesjsonschema.dev'
diff --git a/e2e/testdata/fn-eval/privilege-options/enable-network/.krmginore b/e2e/testdata/fn-eval/privilege-options/enable-network/.krmginore
new file mode 100644
index 00000000..9d7a4007
--- /dev/null
+++ b/e2e/testdata/fn-eval/privilege-options/enable-network/.krmginore
@@ -0,0 +1,1 @@
+.expected
diff --git a/e2e/testdata/fn-eval/privilege-options/enable-network/resources.yaml b/e2e/testdata/fn-eval/privilege-options/enable-network/resources.yaml
new file mode 100644
index 00000000..502aebb0
--- /dev/null
+++ b/e2e/testdata/fn-eval/privilege-options/enable-network/resources.yaml
@@ -0,0 +1,21 @@
+# Copyright 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: some-secret
+  namespace: default
+data:
+  less_sensitive_key: less sensitive data goes here
+  private_key: sensitive data goes here
diff --git a/e2e/testdata/fn-eval/privilege-options/mount/.expected/exec.sh b/e2e/testdata/fn-eval/privilege-options/mount/.expected/exec.sh
new file mode 100644
index 00000000..cf8813c0
--- /dev/null
+++ b/e2e/testdata/fn-eval/privilege-options/mount/.expected/exec.sh
@@ -0,0 +1,30 @@
+#! /bin/bash
+# Copyright 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set -eo pipefail
+# Download schema file
+SCHEMA_DIR="schema/master-standalone"
+mkdir -p "$SCHEMA_DIR"
+curl -sSL 'https://kubernetesjsonschema.dev/master-standalone/configmap-v1.json' -o $SCHEMA_DIR/configmap-v1.json
+
+kpt fn eval \
+--image gcr.io/kpt-fn/kubeval:v0.1 \
+--as-current-user \
+--mount type=bind,src=$(pwd)/schema,dst=/schema-dir \
+-- \
+schema_location=file:///schema-dir
+
+# Remove 'schema' to avoid unwanted diff
+rm -r schema
diff --git a/e2e/testdata/fn-eval/privilege-options/mount/.krmignore b/e2e/testdata/fn-eval/privilege-options/mount/.krmignore
new file mode 100644
index 00000000..5a7b8490
--- /dev/null
+++ b/e2e/testdata/fn-eval/privilege-options/mount/.krmignore
@@ -0,0 +1,2 @@
+schema
+.expected
diff --git a/e2e/testdata/fn-eval/privilege-options/mount/resources.yaml b/e2e/testdata/fn-eval/privilege-options/mount/resources.yaml
new file mode 100644
index 00000000..502aebb0
--- /dev/null
+++ b/e2e/testdata/fn-eval/privilege-options/mount/resources.yaml
@@ -0,0 +1,21 @@
+# Copyright 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: some-secret
+  namespace: default
+data:
+  less_sensitive_key: less sensitive data goes here
+  private_key: sensitive data goes here
diff --git a/e2e/testdata/fn-render/fnconfig-ancestorfn-not-mutate-subpkg-config/.expected/diff.patch b/e2e/testdata/fn-render/fnconfig-ancestorfn-not-mutate-subpkg-config/.expected/diff.patch
new file mode 100644
index 00000000..79e7c68f
--- /dev/null
+++ b/e2e/testdata/fn-render/fnconfig-ancestorfn-not-mutate-subpkg-config/.expected/diff.patch
@@ -0,0 +1,39 @@
+diff --git a/db/resources.yaml b/db/resources.yaml
+index ac1fd96..2fa6e44 100644
+--- a/db/resources.yaml
++++ b/db/resources.yaml
+@@ -15,5 +15,15 @@ apiVersion: apps/v1
+ kind: StatefulSet
+ metadata:
+   name: db
++  namespace: staging
++  labels:
++    tier: db
+ spec:
+   replicas: 3
++  selector:
++    matchLabels:
++      tier: db
++  template:
++    metadata:
++      labels:
++        tier: db
+diff --git a/resources.yaml b/resources.yaml
+index 7a494c9..254b9cd 100644
+--- a/resources.yaml
++++ b/resources.yaml
+@@ -15,6 +15,7 @@ apiVersion: apps/v1
+ kind: Deployment
+ metadata:
+   name: nginx-deployment
++  namespace: staging
+ spec:
+   replicas: 3
+ ---
+@@ -22,5 +23,6 @@ apiVersion: custom.io/v1
+ kind: Custom
+ metadata:
+   name: custom
++  namespace: staging
+ spec:
+   image: nginx:1.2.3
diff --git a/e2e/testdata/fn-render/fnconfig-ancestorfn-not-mutate-subpkg-config/.krmignore b/e2e/testdata/fn-render/fnconfig-ancestorfn-not-mutate-subpkg-config/.krmignore
new file mode 100644
index 00000000..9d7a4007
--- /dev/null
+++ b/e2e/testdata/fn-render/fnconfig-ancestorfn-not-mutate-subpkg-config/.krmignore
@@ -0,0 +1,1 @@
+.expected
diff --git a/e2e/testdata/fn-render/fnconfig-ancestorfn-not-mutate-subpkg-config/Kptfile b/e2e/testdata/fn-render/fnconfig-ancestorfn-not-mutate-subpkg-config/Kptfile
new file mode 100644
index 00000000..c759bf1e
--- /dev/null
+++ b/e2e/testdata/fn-render/fnconfig-ancestorfn-not-mutate-subpkg-config/Kptfile
@@ -0,0 +1,9 @@
+apiVersion: kpt.dev/v1alpha2
+kind: Kptfile
+metadata:
+  name: app-with-db
+pipeline:
+  mutators:
+    - image: gcr.io/kpt-fn/set-namespace:unstable
+      configMap:
+        namespace: staging
diff --git a/e2e/testdata/fn-render/fnconfig-ancestorfn-not-mutate-subpkg-config/db/Kptfile b/e2e/testdata/fn-render/fnconfig-ancestorfn-not-mutate-subpkg-config/db/Kptfile
new file mode 100644
index 00000000..94a14166
--- /dev/null
+++ b/e2e/testdata/fn-render/fnconfig-ancestorfn-not-mutate-subpkg-config/db/Kptfile
@@ -0,0 +1,8 @@
+apiVersion: kpt.dev/v1alpha2
+kind: Kptfile
+metadata:
+  name: db
+pipeline:
+  mutators:
+    - image: gcr.io/kpt-fn/set-label:unstable
+      configPath: labelconfig.yaml
diff --git a/e2e/testdata/fn-render/fnconfig-ancestorfn-not-mutate-subpkg-config/db/labelconfig.yaml b/e2e/testdata/fn-render/fnconfig-ancestorfn-not-mutate-subpkg-config/db/labelconfig.yaml
new file mode 100644
index 00000000..aabf4956
--- /dev/null
+++ b/e2e/testdata/fn-render/fnconfig-ancestorfn-not-mutate-subpkg-config/db/labelconfig.yaml
@@ -0,0 +1,19 @@
+# Copyright 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: label-config
+data:
+  tier: db
diff --git a/e2e/testdata/fn-render/fnconfig-ancestorfn-not-mutate-subpkg-config/db/resources.yaml b/e2e/testdata/fn-render/fnconfig-ancestorfn-not-mutate-subpkg-config/db/resources.yaml
new file mode 100644
index 00000000..ac1fd966
--- /dev/null
+++ b/e2e/testdata/fn-render/fnconfig-ancestorfn-not-mutate-subpkg-config/db/resources.yaml
@@ -0,0 +1,19 @@
+# Copyright 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+apiVersion: apps/v1
+kind: StatefulSet
+metadata:
+  name: db
+spec:
+  replicas: 3
diff --git a/e2e/testdata/fn-render/fnconfig-ancestorfn-not-mutate-subpkg-config/resources.yaml b/e2e/testdata/fn-render/fnconfig-ancestorfn-not-mutate-subpkg-config/resources.yaml
new file mode 100644
index 00000000..7a494c9c
--- /dev/null
+++ b/e2e/testdata/fn-render/fnconfig-ancestorfn-not-mutate-subpkg-config/resources.yaml
@@ -0,0 +1,26 @@
+# Copyright 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: nginx-deployment
+spec:
+  replicas: 3
+---
+apiVersion: custom.io/v1
+kind: Custom
+metadata:
+  name: custom
+spec:
+  image: nginx:1.2.3
diff --git a/e2e/testdata/fn-render/fnconfig-cannot-refer-subpkg b/e2e/testdata/fn-render/fnconfig-cannot-refer-subpkg
new file mode 160000
index 00000000..17877d7a
--- /dev/null
+++ b/e2e/testdata/fn-render/fnconfig-cannot-refer-subpkg
@@ -0,0 +1,1 @@
+Subproject commit 17877d7adaa3ee4433c7b9298b87b5b9d3243a62
diff --git a/e2e/testdata/fn-render/fnconfig-not-relative/.expected/config.yaml b/e2e/testdata/fn-render/fnconfig-not-relative/.expected/config.yaml
new file mode 100644
index 00000000..17f87915
--- /dev/null
+++ b/e2e/testdata/fn-render/fnconfig-not-relative/.expected/config.yaml
@@ -0,0 +1,20 @@
+# Copyright 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+exitCode: 1
+stdErr: |
+  Kptfile is invalid:
+  Field: `pipeline.mutators[0].configPath`
+  Value: "/some/root/labelconfig.yaml"
+  Reason: path must be relative
\ No newline at end of file
diff --git a/e2e/testdata/fn-render/fnconfig-not-relative/.krmignore b/e2e/testdata/fn-render/fnconfig-not-relative/.krmignore
new file mode 100644
index 00000000..9d7a4007
--- /dev/null
+++ b/e2e/testdata/fn-render/fnconfig-not-relative/.krmignore
@@ -0,0 +1,1 @@
+.expected
diff --git a/e2e/testdata/fn-render/fnconfig-not-relative/Kptfile b/e2e/testdata/fn-render/fnconfig-not-relative/Kptfile
new file mode 100644
index 00000000..c42cf1fc
--- /dev/null
+++ b/e2e/testdata/fn-render/fnconfig-not-relative/Kptfile
@@ -0,0 +1,8 @@
+apiVersion: kpt.dev/v1alpha2
+kind: Kptfile
+metadata:
+  name: app-with-db
+pipeline:
+  mutators:
+    - image: gcr.io/kpt-fn/set-label:unstable
+      configPath: /some/root/labelconfig.yaml
diff --git a/e2e/testdata/fn-render/fnconfig-not-relative/labelconfig.yaml b/e2e/testdata/fn-render/fnconfig-not-relative/labelconfig.yaml
new file mode 100644
index 00000000..aabf4956
--- /dev/null
+++ b/e2e/testdata/fn-render/fnconfig-not-relative/labelconfig.yaml
@@ -0,0 +1,19 @@
+# Copyright 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: label-config
+data:
+  tier: db
diff --git a/e2e/testdata/fn-render/fnconfig-not-relative/resources.yaml b/e2e/testdata/fn-render/fnconfig-not-relative/resources.yaml
new file mode 100644
index 00000000..7a494c9c
--- /dev/null
+++ b/e2e/testdata/fn-render/fnconfig-not-relative/resources.yaml
@@ -0,0 +1,26 @@
+# Copyright 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: nginx-deployment
+spec:
+  replicas: 3
+---
+apiVersion: custom.io/v1
+kind: Custom
+metadata:
+  name: custom
+spec:
+  image: nginx:1.2.3
diff --git a/e2e/testdata/fn-render/fnconfig-outside-package/.expected/config.yaml b/e2e/testdata/fn-render/fnconfig-outside-package/.expected/config.yaml
new file mode 100644
index 00000000..af9e9345
--- /dev/null
+++ b/e2e/testdata/fn-render/fnconfig-outside-package/.expected/config.yaml
@@ -0,0 +1,20 @@
+# Copyright 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+exitCode: 1
+stdErr: |
+  Kptfile is invalid:
+  Field: `pipeline.mutators[1].configPath`
+  Value: "some/../../home/.kube/config"
+  Reason: path must not be outside the package
\ No newline at end of file
diff --git a/e2e/testdata/fn-render/fnconfig-outside-package/.krmignore b/e2e/testdata/fn-render/fnconfig-outside-package/.krmignore
new file mode 100644
index 00000000..9d7a4007
--- /dev/null
+++ b/e2e/testdata/fn-render/fnconfig-outside-package/.krmignore
@@ -0,0 +1,1 @@
+.expected
diff --git a/e2e/testdata/fn-render/fnconfig-outside-package/Kptfile b/e2e/testdata/fn-render/fnconfig-outside-package/Kptfile
new file mode 100644
index 00000000..51520938
--- /dev/null
+++ b/e2e/testdata/fn-render/fnconfig-outside-package/Kptfile
@@ -0,0 +1,10 @@
+apiVersion: kpt.dev/v1alpha2
+kind: Kptfile
+metadata:
+  name: app-with-db
+pipeline:
+  mutators:
+    - image: gcr.io/kpt-fn/set-label:unstable
+      configPath: labelconfig.yaml
+    - image: gcr.io/kpt-fn/set-label:unstable
+      configPath: some/../../home/.kube/config # config pointing outside the package
diff --git a/e2e/testdata/fn-render/fnconfig-outside-package/labelconfig.yaml b/e2e/testdata/fn-render/fnconfig-outside-package/labelconfig.yaml
new file mode 100644
index 00000000..aabf4956
--- /dev/null
+++ b/e2e/testdata/fn-render/fnconfig-outside-package/labelconfig.yaml
@@ -0,0 +1,19 @@
+# Copyright 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: label-config
+data:
+  tier: db
diff --git a/e2e/testdata/fn-render/fnconfig-outside-package/resources.yaml b/e2e/testdata/fn-render/fnconfig-outside-package/resources.yaml
new file mode 100644
index 00000000..7a494c9c
--- /dev/null
+++ b/e2e/testdata/fn-render/fnconfig-outside-package/resources.yaml
@@ -0,0 +1,26 @@
+# Copyright 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: nginx-deployment
+spec:
+  replicas: 3
+---
+apiVersion: custom.io/v1
+kind: Custom
+metadata:
+  name: custom
+spec:
+  image: nginx:1.2.3
diff --git a/e2e/testdata/fn-render/fnconfig-pkgfn-refers-subdir/.expected/diff.patch b/e2e/testdata/fn-render/fnconfig-pkgfn-refers-subdir/.expected/diff.patch
new file mode 100644
index 00000000..c0929f05
--- /dev/null
+++ b/e2e/testdata/fn-render/fnconfig-pkgfn-refers-subdir/.expected/diff.patch
@@ -0,0 +1,28 @@
+diff --git a/resources.yaml b/resources.yaml
+index 7a494c9..d22aec8 100644
+--- a/resources.yaml
++++ b/resources.yaml
+@@ -15,12 +15,23 @@ apiVersion: apps/v1
+ kind: Deployment
+ metadata:
+   name: nginx-deployment
++  labels:
++    tier: db
+ spec:
+   replicas: 3
++  selector:
++    matchLabels:
++      tier: db
++  template:
++    metadata:
++      labels:
++        tier: db
+ ---
+ apiVersion: custom.io/v1
+ kind: Custom
+ metadata:
+   name: custom
++  labels:
++    tier: db
+ spec:
+   image: nginx:1.2.3
diff --git a/e2e/testdata/fn-render/fnconfig-pkgfn-refers-subdir/.krmignore b/e2e/testdata/fn-render/fnconfig-pkgfn-refers-subdir/.krmignore
new file mode 100644
index 00000000..9d7a4007
--- /dev/null
+++ b/e2e/testdata/fn-render/fnconfig-pkgfn-refers-subdir/.krmignore
@@ -0,0 +1,1 @@
+.expected
diff --git a/e2e/testdata/fn-render/fnconfig-pkgfn-refers-subdir/Kptfile b/e2e/testdata/fn-render/fnconfig-pkgfn-refers-subdir/Kptfile
new file mode 100644
index 00000000..33a4e86a
--- /dev/null
+++ b/e2e/testdata/fn-render/fnconfig-pkgfn-refers-subdir/Kptfile
@@ -0,0 +1,8 @@
+apiVersion: kpt.dev/v1alpha2
+kind: Kptfile
+metadata:
+  name: app-with-db
+pipeline:
+  mutators:
+    - image: gcr.io/kpt-fn/set-label:unstable
+      configPath: confs/labelconfig.yaml
diff --git a/e2e/testdata/fn-render/fnconfig-pkgfn-refers-subdir/confs/labelconfig.yaml b/e2e/testdata/fn-render/fnconfig-pkgfn-refers-subdir/confs/labelconfig.yaml
new file mode 100644
index 00000000..aabf4956
--- /dev/null
+++ b/e2e/testdata/fn-render/fnconfig-pkgfn-refers-subdir/confs/labelconfig.yaml
@@ -0,0 +1,19 @@
+# Copyright 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: label-config
+data:
+  tier: db
diff --git a/e2e/testdata/fn-render/fnconfig-pkgfn-refers-subdir/resources.yaml b/e2e/testdata/fn-render/fnconfig-pkgfn-refers-subdir/resources.yaml
new file mode 100644
index 00000000..7a494c9c
--- /dev/null
+++ b/e2e/testdata/fn-render/fnconfig-pkgfn-refers-subdir/resources.yaml
@@ -0,0 +1,26 @@
+# Copyright 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: nginx-deployment
+spec:
+  replicas: 3
+---
+apiVersion: custom.io/v1
+kind: Custom
+metadata:
+  name: custom
+spec:
+  image: nginx:1.2.3
diff --git a/e2e/testdata/fn-render/fnresult-fn-failure/.expected/results.yaml b/e2e/testdata/fn-render/fnresult-fn-failure/.expected/results.yaml
index 023dcf41..f24539a7 100755
--- a/e2e/testdata/fn-render/fnresult-fn-failure/.expected/results.yaml
+++ b/e2e/testdata/fn-render/fnresult-fn-failure/.expected/results.yaml
@@ -21,6 +21,7 @@ items:
           resourceRef:
             apiVersion: apps/v1
             kind: Deployment
+            name: nginx-deployment
           field:
             path: selector
           file:
@@ -30,6 +31,7 @@ items:
           resourceRef:
             apiVersion: apps/v1
             kind: Deployment
+            name: nginx-deployment
           field:
             path: template
           file:
@@ -39,6 +41,7 @@ items:
           resourceRef:
             apiVersion: apps/v1
             kind: Deployment
+            name: nginx-deployment
           field:
             path: spec.replicas
           file:
@@ -52,6 +55,7 @@ items:
           resourceRef:
             apiVersion: custom.io/v1
             kind: Custom
+            name: custom
           file:
             path: resources.yaml
             index: 1
diff --git a/e2e/testdata/fn-render/kubeval-failure/.expected/config.yaml b/e2e/testdata/fn-render/kubeval-failure/.expected/config.yaml
index b257d428..5e58752b 100644
--- a/e2e/testdata/fn-render/kubeval-failure/.expected/config.yaml
+++ b/e2e/testdata/fn-render/kubeval-failure/.expected/config.yaml
@@ -15,3 +15,4 @@
 # One of the functions in the pipeline fails resulting in
 # non-zero exit code and no changes in the resources.
 exitCode: 1
+stdOut: "[ERROR] selector is required in object \"apps/v1/Deployment/nginx-deployment\" in file \"resources.yaml\" in field \"selector\""
diff --git a/e2e/testdata/fn-render/kubeval-failure/.expected/results.yaml b/e2e/testdata/fn-render/kubeval-failure/.expected/results.yaml
index 929e572a..352d463d 100755
--- a/e2e/testdata/fn-render/kubeval-failure/.expected/results.yaml
+++ b/e2e/testdata/fn-render/kubeval-failure/.expected/results.yaml
@@ -12,6 +12,7 @@ items:
           resourceRef:
             apiVersion: apps/v1
             kind: Deployment
+            name: nginx-deployment
           field:
             path: selector
           file:
@@ -21,6 +22,7 @@ items:
           resourceRef:
             apiVersion: apps/v1
             kind: Deployment
+            name: nginx-deployment
           field:
             path: template
           file:
@@ -30,6 +32,7 @@ items:
           resourceRef:
             apiVersion: apps/v1
             kind: Deployment
+            name: nginx-deployment
           field:
             path: spec.replicas
           file:
@@ -41,6 +44,7 @@ items:
           resourceRef:
             apiVersion: custom.io/v1
             kind: Custom
+            name: custom
           file:
             path: resources.yaml
             index: 1
diff --git a/e2e/testdata/fn-render/missing-fnconfig/.expected/config.yaml b/e2e/testdata/fn-render/missing-fnconfig/.expected/config.yaml
index a415bb95..be2387df 100644
--- a/e2e/testdata/fn-render/missing-fnconfig/.expected/config.yaml
+++ b/e2e/testdata/fn-render/missing-fnconfig/.expected/config.yaml
@@ -13,7 +13,11 @@
 # limitations under the License.
 
 exitCode: 1
-stdErr: 'Error: missing function config "labelconfig1.yaml"'
+stdErr: |
+  Kptfile is invalid:
+  Field: `pipeline.mutators[1].configPath`
+  Value: "labelconfig1.yaml"
+  Reason: functionConfig must exist in the current package
 stdOut: |
   [RUNNING] "gcr.io/kpt-fn/set-labels:unstable"
   [PASS] "gcr.io/kpt-fn/set-labels:unstable"
diff --git a/internal/errors/resolver/resolver_test.go b/internal/errors/resolver/resolver_test.go
new file mode 100644
index 00000000..6153b14e
--- /dev/null
+++ b/internal/errors/resolver/resolver_test.go
@@ -0,0 +1,50 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package resolver
+
+import (
+	"testing"
+
+	"github.com/GoogleContainerTools/kpt/internal/errors"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestResolveError_DefaultExitCode(t *testing.T) {
+	org := errorResolvers
+	AddErrorResolver(&TestErrorResolver{})
+	defer func() {
+		errorResolvers = org
+	}()
+
+	rr, ok := ResolveError(&TestError{})
+	assert.True(t, ok)
+	assert.Equal(t, 1, rr.ExitCode)
+}
+
+type TestErrorResolver struct{}
+
+func (t *TestErrorResolver) Resolve(err error) (ResolvedResult, bool) {
+	var testError *TestError
+	if errors.As(err, &testError) {
+		return ResolvedResult{}, true
+	}
+	return ResolvedResult{}, false
+}
+
+type TestError struct{}
+
+func (e *TestError) Error() string {
+	return "this is a test"
+}
diff --git a/internal/fnruntime/runner_test.go b/internal/fnruntime/runner_test.go
index 3ec3dd6f..c2d36c4d 100644
--- a/internal/fnruntime/runner_test.go
+++ b/internal/fnruntime/runner_test.go
@@ -24,6 +24,8 @@ import (
 	"strings"
 	"testing"
 
+	fnresult "github.com/GoogleContainerTools/kpt/pkg/api/fnresult/v1alpha2"
+
 	"github.com/GoogleContainerTools/kpt/internal/types"
 	kptfilev1alpha2 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1alpha2"
 	"github.com/stretchr/testify/assert"
@@ -447,3 +449,147 @@ spec:
 		}
 	}
 }
+
+func TestGetResourceRefMetadata(t *testing.T) {
+	tests := map[string]struct {
+		input    string // input
+		expected string // expected result
+	}{
+		"new format with name": {
+			input: `
+message: selector is required
+severity: error
+resourceRef:
+    apiVersion: apps/v1
+    kind: Deployment
+    name: nginx-deployment
+field:
+    path: selector
+file:
+    path: resources.yaml
+`,
+			expected: `message: selector is required
+severity: error
+resourceRef:
+    apiVersion: apps/v1
+    kind: Deployment
+    name: nginx-deployment
+field:
+    path: selector
+file:
+    path: resources.yaml
+`,
+		},
+		"new format with namespace": {
+			input: `
+message: selector is required
+severity: error
+resourceRef:
+    apiVersion: apps/v1
+    kind: Deployment
+    name: nginx-deployment
+    namespace: my-namespace
+field:
+    path: selector
+file:
+    path: resources.yaml
+`,
+			expected: `message: selector is required
+severity: error
+resourceRef:
+    apiVersion: apps/v1
+    kind: Deployment
+    name: nginx-deployment
+    namespace: my-namespace
+field:
+    path: selector
+file:
+    path: resources.yaml
+`,
+		},
+		"old format with name": {
+			input: `
+message: selector is required
+severity: error
+resourceRef:
+    apiVersion: apps/v1
+    kind: Deployment
+    metadata:
+      name: nginx-deployment
+field:
+    path: selector
+file:
+    path: resources.yaml
+`,
+			expected: `message: selector is required
+severity: error
+resourceRef:
+    apiVersion: apps/v1
+    kind: Deployment
+    name: nginx-deployment
+field:
+    path: selector
+file:
+    path: resources.yaml
+`,
+		},
+		"old format with namespace": {
+			input: `
+message: selector is required
+severity: error
+resourceRef:
+    apiVersion: apps/v1
+    kind: Deployment
+    metadata:
+      name: nginx-deployment
+      namespace: my-namespace
+field:
+    path: selector
+file:
+    path: resources.yaml
+`,
+			expected: `message: selector is required
+severity: error
+resourceRef:
+    apiVersion: apps/v1
+    kind: Deployment
+    name: nginx-deployment
+    namespace: my-namespace
+field:
+    path: selector
+file:
+    path: resources.yaml
+`,
+		},
+		"no resourceRef": {
+			input: `
+message: selector is required
+severity: error
+field:
+    path: selector
+file:
+    path: resources.yaml
+`,
+			expected: `message: selector is required
+severity: error
+field:
+    path: selector
+file:
+    path: resources.yaml
+`,
+		},
+	}
+	for _, tc := range tests {
+		yml, err := yaml.Parse(tc.input)
+		assert.NoError(t, err)
+
+		result := &fnresult.ResultItem{}
+		err = yaml.Unmarshal([]byte(tc.input), result)
+		assert.NoError(t, err)
+		assert.NoError(t, populateResourceRef(yml, result))
+
+		out, err := yaml.Marshal(result)
+		assert.NoError(t, err)
+		assert.Equal(t, tc.expected, string(out))
+	}
+}
diff --git a/pkg/api/kptfile/v1alpha2/validation_test.go b/pkg/api/kptfile/v1alpha2/validation_test.go
index 22600dd2..3c04002f 100644
--- a/pkg/api/kptfile/v1alpha2/validation_test.go
+++ b/pkg/api/kptfile/v1alpha2/validation_test.go
@@ -111,6 +111,62 @@ metadata:
 			},
 			valid: false,
 		},
+		{
+			name: "pipeline: configpath referring file in parent",
+			kptfile: KptFile{
+				Pipeline: &Pipeline{
+					Mutators: []Function{
+						{
+							Image:      "image",
+							ConfigPath: "../config.yaml",
+						},
+					},
+				},
+			},
+			valid: false,
+		},
+		{
+			name: "pipeline: cleaned configpath does not contain ..",
+			kptfile: KptFile{
+				Pipeline: &Pipeline{
+					Mutators: []Function{
+						{
+							Image:      "image",
+							ConfigPath: "a/b/../config.yaml",
+						},
+					},
+				},
+			},
+			valid: true,
+		},
+		{
+			name: "pipeline: cleaned configpath contains ..",
+			kptfile: KptFile{
+				Pipeline: &Pipeline{
+					Mutators: []Function{
+						{
+							Image:      "image",
+							ConfigPath: "a/b/../../../config.yaml",
+						},
+					},
+				},
+			},
+			valid: false,
+		},
+		{
+			name: "pipeline: configpath contains invalid .. references",
+			kptfile: KptFile{
+				Pipeline: &Pipeline{
+					Mutators: []Function{
+						{
+							Image:      "image",
+							ConfigPath: "a/.../config.yaml",
+						},
+					},
+				},
+			},
+			valid: false,
+		},
 	}
 
 	for _, c := range cases {
@@ -236,7 +292,7 @@ func TestValidatePath(t *testing.T) {
 		},
 		{
 			"./a/.../b",
-			true,
+			false,
 		},
 		{
 			".",
@@ -244,23 +300,23 @@ func TestValidatePath(t *testing.T) {
 		},
 		{
 			"a\\b",
-			false,
+			true,
 		},
 		{
 			"a\b",
-			false,
+			true,
 		},
 		{
 			"a\v",
-			false,
+			true,
 		},
 		{
 			"a:\\b\\c",
-			false,
+			true,
 		},
 		{
 			"../a/../b",
-			true,
+			false,
 		},
 		{
 			"a//b",
@@ -270,26 +326,10 @@ func TestValidatePath(t *testing.T) {
 			"a/b/.",
 			true,
 		},
-		{
-			"a/*/b",
-			false,
-		},
-		{
-			"./*",
-			true,
-		},
-		{
-			"a/b\\c",
-			false,
-		},
 		{
 			"././././",
 			true,
 		},
-		{
-			"./!&^%$/#(@)/_-=+|<;>?:'\"/'`",
-			true,
-		},
 		{
 			"",
 			false,
@@ -298,16 +338,12 @@ func TestValidatePath(t *testing.T) {
 			"\t \n",
 			false,
 		},
-		{
-			"*",
-			false,
-		},
 	}
 
 	for _, c := range cases {
-		ret := validatePath(c.Path)
+		ret := validateFnConfigPath(c.Path)
 		if (ret == nil) != c.Valid {
-			t.Fatalf("returned value for path %s should be %t, got %t",
+			t.Fatalf("returned value for path %q should be %t, got %t",
 				c.Path, c.Valid, (ret == nil))
 		}
 	}
diff --git a/thirdparty/cli-utils/printers/events/formatter_test.go b/thirdparty/cli-utils/printers/events/formatter_test.go
index d946eb0a..b995d29d 100644
--- a/thirdparty/cli-utils/printers/events/formatter_test.go
+++ b/thirdparty/cli-utils/printers/events/formatter_test.go
@@ -32,7 +32,6 @@ func TestFormatter_FormatApplyEvent(t *testing.T) {
 			dryRunStrategy: common.DryRunNone,
 			event: event.ApplyEvent{
 				Operation:  event.Created,
-				Type:       event.ApplyEventResourceUpdate,
 				Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"),
 			},
 			expected: "deployment.apps/my-dep created",
@@ -41,7 +40,6 @@ func TestFormatter_FormatApplyEvent(t *testing.T) {
 			dryRunStrategy: common.DryRunClient,
 			event: event.ApplyEvent{
 				Operation:  event.Configured,
-				Type:       event.ApplyEventResourceUpdate,
 				Identifier: createIdentifier("apps", "Deployment", "", "my-dep"),
 			},
 			expected: "deployment.apps/my-dep configured (dry-run)",
@@ -50,7 +48,6 @@ func TestFormatter_FormatApplyEvent(t *testing.T) {
 			dryRunStrategy: common.DryRunServer,
 			event: event.ApplyEvent{
 				Operation:  event.Configured,
-				Type:       event.ApplyEventResourceUpdate,
 				Identifier: createIdentifier("batch", "CronJob", "foo", "my-cron"),
 			},
 			expected: "cronjob.batch/my-cron configured (dry-run-server)",
@@ -58,42 +55,10 @@ func TestFormatter_FormatApplyEvent(t *testing.T) {
 		"apply event with error should display the error": {
 			dryRunStrategy: common.DryRunServer,
 			event: event.ApplyEvent{
-				Operation:  event.Failed,
-				Type:       event.ApplyEventResourceUpdate,
 				Identifier: createIdentifier("apps", "Deployment", "", "my-dep"),
 				Error:      fmt.Errorf("this is a test error"),
 			},
-			expected: "deployment.apps/my-dep failed: this is a test error (dry-run-server)",
-		},
-		"completed event": {
-			dryRunStrategy: common.DryRunNone,
-			event: event.ApplyEvent{
-				Type: event.ApplyEventCompleted,
-			},
-			applyStats: &list.ApplyStats{
-				ServersideApplied: 1,
-			},
-			statusCollector: &fakeCollector{
-				m: map[object.ObjMetadata]event.StatusEvent{
-					{ //nolint:gofmt
-						GroupKind: schema.GroupKind{
-							Group: "apps",
-							Kind:  "Deployment",
-						},
-						Namespace: "foo",
-						Name:      "my-dep",
-					}: {
-						Resource: &pollevent.ResourceStatus{
-							Status:  status.CurrentStatus,
-							Message: "Resource is Current",
-						},
-					},
-				},
-			},
-			expected: `
-1 resource(s) applied. 0 created, 0 unchanged, 0 configured, 0 failed, 1 serverside applied
-deployment.apps/my-dep is Current: Resource is Current
-`,
+			expected: "deployment.apps/my-dep apply failed: this is a test error (dry-run-server)",
 		},
 	}
 
@@ -101,7 +66,7 @@ deployment.apps/my-dep is Current: Resource is Current
 		t.Run(tn, func(t *testing.T) {
 			ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled
 			formatter := NewFormatter(ioStreams, tc.dryRunStrategy)
-			err := formatter.FormatApplyEvent(tc.event, tc.applyStats, tc.statusCollector)
+			err := formatter.FormatApplyEvent(tc.event)
 			assert.NoError(t, err)
 
 			assert.Equal(t, strings.TrimSpace(tc.expected), strings.TrimSpace(out.String()))
@@ -119,8 +84,15 @@ func TestFormatter_FormatStatusEvent(t *testing.T) {
 		"resource update with Current status": {
 			dryRunStrategy: common.DryRunNone,
 			event: event.StatusEvent{
-				Type: event.StatusEventResourceUpdate,
-				Resource: &pollevent.ResourceStatus{
+				Identifier: object.ObjMetadata{
+					GroupKind: schema.GroupKind{
+						Group: "apps",
+						Kind:  "Deployment",
+					},
+					Namespace: "foo",
+					Name:      "bar",
+				},
+				PollResourceInfo: &pollevent.ResourceStatus{
 					Identifier: object.ObjMetadata{
 						GroupKind: schema.GroupKind{
 							Group: "apps",
@@ -141,7 +113,7 @@ func TestFormatter_FormatStatusEvent(t *testing.T) {
 		t.Run(tn, func(t *testing.T) {
 			ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled
 			formatter := NewFormatter(ioStreams, tc.dryRunStrategy)
-			err := formatter.FormatStatusEvent(tc.event, tc.statusCollector)
+			err := formatter.FormatStatusEvent(tc.event)
 			assert.NoError(t, err)
 
 			assert.Equal(t, strings.TrimSpace(tc.expected), strings.TrimSpace(out.String()))
@@ -160,7 +132,6 @@ func TestFormatter_FormatPruneEvent(t *testing.T) {
 			dryRunStrategy: common.DryRunNone,
 			event: event.PruneEvent{
 				Operation:  event.Pruned,
-				Type:       event.PruneEventResourceUpdate,
 				Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"),
 			},
 			expected: "deployment.apps/my-dep pruned",
@@ -169,7 +140,6 @@ func TestFormatter_FormatPruneEvent(t *testing.T) {
 			dryRunStrategy: common.DryRunClient,
 			event: event.PruneEvent{
 				Operation:  event.PruneSkipped,
-				Type:       event.PruneEventResourceUpdate,
 				Identifier: createIdentifier("apps", "Deployment", "", "my-dep"),
 			},
 			expected: "deployment.apps/my-dep prune skipped (dry-run)",
@@ -177,30 +147,18 @@ func TestFormatter_FormatPruneEvent(t *testing.T) {
 		"resource with prune error": {
 			dryRunStrategy: common.DryRunNone,
 			event: event.PruneEvent{
-				Type:       event.PruneEventFailed,
 				Identifier: createIdentifier("apps", "Deployment", "", "my-dep"),
 				Error:      fmt.Errorf("this is a test"),
 			},
 			expected: "deployment.apps/my-dep prune failed: this is a test",
 		},
-		"prune event with completed status": {
-			dryRunStrategy: common.DryRunNone,
-			event: event.PruneEvent{
-				Type: event.PruneEventCompleted,
-			},
-			pruneStats: &list.PruneStats{
-				Pruned:  1,
-				Skipped: 2,
-			},
-			expected: "1 resource(s) pruned, 2 skipped, 0 failed",
-		},
 	}
 
 	for tn, tc := range testCases {
 		t.Run(tn, func(t *testing.T) {
 			ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled
 			formatter := NewFormatter(ioStreams, tc.dryRunStrategy)
-			err := formatter.FormatPruneEvent(tc.event, tc.pruneStats)
+			err := formatter.FormatPruneEvent(tc.event)
 			assert.NoError(t, err)
 
 			assert.Equal(t, strings.TrimSpace(tc.expected), strings.TrimSpace(out.String()))
@@ -219,49 +177,37 @@ func TestFormatter_FormatDeleteEvent(t *testing.T) {
 		"resource deleted without no dryrun": {
 			dryRunStrategy: common.DryRunNone,
 			event: event.DeleteEvent{
-				Operation: event.Deleted,
-				Type:      event.DeleteEventResourceUpdate,
-				Object:    createObject("apps", "Deployment", "default", "my-dep"),
+				Operation:  event.Deleted,
+				Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"),
+				Object:     createObject("apps", "Deployment", "default", "my-dep"),
 			},
 			expected: "deployment.apps/my-dep deleted",
 		},
 		"resource skipped with client dryrun": {
 			dryRunStrategy: common.DryRunClient,
 			event: event.DeleteEvent{
-				Operation: event.DeleteSkipped,
-				Type:      event.DeleteEventResourceUpdate,
-				Object:    createObject("apps", "Deployment", "", "my-dep"),
+				Operation:  event.DeleteSkipped,
+				Identifier: createIdentifier("apps", "Deployment", "", "my-dep"),
+				Object:     createObject("apps", "Deployment", "", "my-dep"),
 			},
 			expected: "deployment.apps/my-dep delete skipped (dry-run)",
 		},
 		"resource with delete error": {
 			dryRunStrategy: common.DryRunServer,
 			event: event.DeleteEvent{
-				Type:       event.DeleteEventFailed,
 				Object:     createObject("apps", "Deployment", "", "my-dep"),
 				Identifier: createIdentifier("apps", "Deployment", "", "my-dep"),
 				Error:      fmt.Errorf("this is a test"),
 			},
 			expected: "deployment.apps/my-dep deletion failed: this is a test (dry-run-server)",
 		},
-		"delete event with completed status": {
-			dryRunStrategy: common.DryRunNone,
-			event: event.DeleteEvent{
-				Type: event.DeleteEventCompleted,
-			},
-			deleteStats: &list.DeleteStats{
-				Deleted: 1,
-				Skipped: 2,
-			},
-			expected: "1 resource(s) deleted, 2 skipped",
-		},
 	}
 
 	for tn, tc := range testCases {
 		t.Run(tn, func(t *testing.T) {
 			ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled
 			formatter := NewFormatter(ioStreams, tc.dryRunStrategy)
-			err := formatter.FormatDeleteEvent(tc.event, tc.deleteStats)
+			err := formatter.FormatDeleteEvent(tc.event)
 			assert.NoError(t, err)
 
 			assert.Equal(t, strings.TrimSpace(tc.expected), strings.TrimSpace(out.String()))
@@ -292,11 +238,3 @@ func createIdentifier(group, kind, namespace, name string) object.ObjMetadata {
 		},
 	}
 }
-
-type fakeCollector struct {
-	m map[object.ObjMetadata]event.StatusEvent
-}
-
-func (f *fakeCollector) LatestStatus() map[object.ObjMetadata]event.StatusEvent {
-	return f.m
-}
diff --git a/thirdparty/cli-utils/printers/json/formatter_test.go b/thirdparty/cli-utils/printers/json/formatter_test.go
index 269bb00d..d80d69ae 100644
--- a/thirdparty/cli-utils/printers/json/formatter_test.go
+++ b/thirdparty/cli-utils/printers/json/formatter_test.go
@@ -21,17 +21,14 @@ import (
 
 func TestFormatter_FormatApplyEvent(t *testing.T) {
 	testCases := map[string]struct {
-		dryRunStrategy  common.DryRunStrategy
-		event           event.ApplyEvent
-		applyStats      *list.ApplyStats
-		statusCollector list.Collector
-		expected        []map[string]interface{}
+		dryRunStrategy common.DryRunStrategy
+		event          event.ApplyEvent
+		expected       []map[string]interface{}
 	}{
 		"resource created without dryrun": {
 			dryRunStrategy: common.DryRunNone,
 			event: event.ApplyEvent{
 				Operation:  event.Created,
-				Type:       event.ApplyEventResourceUpdate,
 				Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"),
 			},
 			expected: []map[string]interface{}{
@@ -51,7 +48,6 @@ func TestFormatter_FormatApplyEvent(t *testing.T) {
 			dryRunStrategy: common.DryRunClient,
 			event: event.ApplyEvent{
 				Operation:  event.Configured,
-				Type:       event.ApplyEventResourceUpdate,
 				Identifier: createIdentifier("apps", "Deployment", "", "my-dep"),
 			},
 			expected: []map[string]interface{}{
@@ -71,7 +67,6 @@ func TestFormatter_FormatApplyEvent(t *testing.T) {
 			dryRunStrategy: common.DryRunServer,
 			event: event.ApplyEvent{
 				Operation:  event.Configured,
-				Type:       event.ApplyEventResourceUpdate,
 				Identifier: createIdentifier("batch", "CronJob", "foo", "my-cron"),
 			},
 			expected: []map[string]interface{}{
@@ -87,63 +82,13 @@ func TestFormatter_FormatApplyEvent(t *testing.T) {
 				},
 			},
 		},
-		"completed event": {
-			dryRunStrategy: common.DryRunNone,
-			event: event.ApplyEvent{
-				Type: event.ApplyEventCompleted,
-			},
-			applyStats: &list.ApplyStats{
-				ServersideApplied: 1,
-			},
-			statusCollector: &fakeCollector{
-				m: map[object.ObjMetadata]event.StatusEvent{
-					{ //nolint:gofmt
-						GroupKind: schema.GroupKind{
-							Group: "apps",
-							Kind:  "Deployment",
-						},
-						Namespace: "foo",
-						Name:      "my-dep",
-					}: {
-						Resource: &pollevent.ResourceStatus{
-							Status:  status.CurrentStatus,
-							Message: "Resource is Current",
-						},
-					},
-				},
-			},
-			expected: []map[string]interface{}{
-				{
-					"configuredCount": 0,
-					"count":           1,
-					"createdCount":    0,
-					"eventType":       "completed",
-					"failedCount":     0,
-					"serverSideCount": 1,
-					"type":            "apply",
-					"unchangedCount":  0,
-					"timestamp":       "",
-				},
-				{
-					"eventType": "resourceStatus",
-					"group":     "apps",
-					"kind":      "Deployment",
-					"message":   "Resource is Current",
-					"name":      "my-dep",
-					"namespace": "foo",
-					"status":    "Current",
-					"timestamp": "",
-					"type":      "status",
-				},
-			},
-		},
 	}
 
 	for tn, tc := range testCases {
 		t.Run(tn, func(t *testing.T) {
 			ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled
 			formatter := NewFormatter(ioStreams, tc.dryRunStrategy)
-			err := formatter.FormatApplyEvent(tc.event, tc.applyStats, tc.statusCollector)
+			err := formatter.FormatApplyEvent(tc.event)
 			assert.NoError(t, err)
 
 			objects := strings.Split(strings.TrimSpace(out.String()), "\n")
@@ -160,16 +105,22 @@ func TestFormatter_FormatApplyEvent(t *testing.T) {
 
 func TestFormatter_FormatStatusEvent(t *testing.T) {
 	testCases := map[string]struct {
-		dryRunStrategy  common.DryRunStrategy
+		previewStrategy common.DryRunStrategy
 		event           event.StatusEvent
-		statusCollector list.Collector
 		expected        map[string]interface{}
 	}{
 		"resource update with Current status": {
-			dryRunStrategy: common.DryRunNone,
+			previewStrategy: common.DryRunNone,
 			event: event.StatusEvent{
-				Type: event.StatusEventResourceUpdate,
-				Resource: &pollevent.ResourceStatus{
+				Identifier: object.ObjMetadata{
+					GroupKind: schema.GroupKind{
+						Group: "apps",
+						Kind:  "Deployment",
+					},
+					Namespace: "foo",
+					Name:      "bar",
+				},
+				PollResourceInfo: &pollevent.ResourceStatus{
 					Identifier: object.ObjMetadata{
 						GroupKind: schema.GroupKind{
 							Group: "apps",
@@ -199,8 +150,8 @@ func TestFormatter_FormatStatusEvent(t *testing.T) {
 	for tn, tc := range testCases {
 		t.Run(tn, func(t *testing.T) {
 			ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled
-			formatter := NewFormatter(ioStreams, tc.dryRunStrategy)
-			err := formatter.FormatStatusEvent(tc.event, tc.statusCollector)
+			formatter := NewFormatter(ioStreams, tc.previewStrategy)
+			err := formatter.FormatStatusEvent(tc.event)
 			assert.NoError(t, err)
 
 			assertOutput(t, tc.expected, out.String())
@@ -219,7 +170,6 @@ func TestFormatter_FormatPruneEvent(t *testing.T) {
 			dryRunStrategy: common.DryRunNone,
 			event: event.PruneEvent{
 				Operation:  event.Pruned,
-				Type:       event.PruneEventResourceUpdate,
 				Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"),
 			},
 			expected: map[string]interface{}{
@@ -237,7 +187,6 @@ func TestFormatter_FormatPruneEvent(t *testing.T) {
 			dryRunStrategy: common.DryRunClient,
 			event: event.PruneEvent{
 				Operation:  event.PruneSkipped,
-				Type:       event.PruneEventResourceUpdate,
 				Identifier: createIdentifier("apps", "Deployment", "", "my-dep"),
 			},
 			expected: map[string]interface{}{
@@ -251,30 +200,13 @@ func TestFormatter_FormatPruneEvent(t *testing.T) {
 				"type":      "prune",
 			},
 		},
-		"prune event with completed status": {
-			dryRunStrategy: common.DryRunNone,
-			event: event.PruneEvent{
-				Type: event.PruneEventCompleted,
-			},
-			pruneStats: &list.PruneStats{
-				Pruned:  1,
-				Skipped: 2,
-			},
-			expected: map[string]interface{}{
-				"eventType": "completed",
-				"skipped":   2,
-				"pruned":    1,
-				"timestamp": "",
-				"type":      "prune",
-			},
-		},
 	}
 
 	for tn, tc := range testCases {
 		t.Run(tn, func(t *testing.T) {
 			ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled
 			formatter := NewFormatter(ioStreams, tc.dryRunStrategy)
-			err := formatter.FormatPruneEvent(tc.event, tc.pruneStats)
+			err := formatter.FormatPruneEvent(tc.event)
 			assert.NoError(t, err)
 
 			assertOutput(t, tc.expected, out.String())
@@ -294,7 +226,6 @@ func TestFormatter_FormatDeleteEvent(t *testing.T) {
 			dryRunStrategy: common.DryRunNone,
 			event: event.DeleteEvent{
 				Operation:  event.Deleted,
-				Type:       event.DeleteEventResourceUpdate,
 				Identifier: createIdentifier("apps", "Deployment", "default", "my-dep"),
 			},
 			expected: map[string]interface{}{
@@ -312,7 +243,6 @@ func TestFormatter_FormatDeleteEvent(t *testing.T) {
 			dryRunStrategy: common.DryRunClient,
 			event: event.DeleteEvent{
 				Operation:  event.DeleteSkipped,
-				Type:       event.DeleteEventResourceUpdate,
 				Identifier: createIdentifier("apps", "Deployment", "", "my-dep"),
 			},
 			expected: map[string]interface{}{
@@ -326,30 +256,13 @@ func TestFormatter_FormatDeleteEvent(t *testing.T) {
 				"type":      "delete",
 			},
 		},
-		"delete event with completed status": {
-			dryRunStrategy: common.DryRunNone,
-			event: event.DeleteEvent{
-				Type: event.DeleteEventCompleted,
-			},
-			deleteStats: &list.DeleteStats{
-				Deleted: 1,
-				Skipped: 2,
-			},
-			expected: map[string]interface{}{
-				"deleted":   1,
-				"eventType": "completed",
-				"skipped":   2,
-				"timestamp": "",
-				"type":      "delete",
-			},
-		},
 	}
 
 	for tn, tc := range testCases {
 		t.Run(tn, func(t *testing.T) {
 			ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled
 			formatter := NewFormatter(ioStreams, tc.dryRunStrategy)
-			err := formatter.FormatDeleteEvent(tc.event, tc.deleteStats)
+			err := formatter.FormatDeleteEvent(tc.event)
 			assert.NoError(t, err)
 
 			assertOutput(t, tc.expected, out.String())
@@ -394,11 +307,3 @@ func createIdentifier(group, kind, namespace, name string) object.ObjMetadata {
 		},
 	}
 }
-
-type fakeCollector struct {
-	m map[object.ObjMetadata]event.StatusEvent
-}
-
-func (f *fakeCollector) LatestStatus() map[object.ObjMetadata]event.StatusEvent {
-	return f.m
-}
diff --git a/thirdparty/cli-utils/printers/table/collector_test.go b/thirdparty/cli-utils/printers/table/collector_test.go
index 2a4511e6..2a561821 100644
--- a/thirdparty/cli-utils/printers/table/collector_test.go
+++ b/thirdparty/cli-utils/printers/table/collector_test.go
@@ -35,15 +35,15 @@ const testMessage = "test message for ResourceStatus"
 
 func TestResourceStateCollector_New(t *testing.T) {
 	testCases := map[string]struct {
-		resourceGroups []event.ResourceGroup
+		resourceGroups []event.ActionGroup
 		resourceInfos  map[object.ObjMetadata]*ResourceInfo
 	}{
 		"no resources": {
-			resourceGroups: []event.ResourceGroup{},
+			resourceGroups: []event.ActionGroup{},
 			resourceInfos:  map[object.ObjMetadata]*ResourceInfo{},
 		},
 		"several resources for apply": {
-			resourceGroups: []event.ResourceGroup{
+			resourceGroups: []event.ActionGroup{
 				{
 					Action: event.ApplyAction,
 					Identifiers: []object.ObjMetadata{
@@ -61,7 +61,7 @@ func TestResourceStateCollector_New(t *testing.T) {
 			},
 		},
 		"several resources for prune": {
-			resourceGroups: []event.ResourceGroup{
+			resourceGroups: []event.ActionGroup{
 				{
 					Action: event.ApplyAction,
 					Identifiers: []object.ObjMetadata{
@@ -104,39 +104,30 @@ func TestResourceStateCollector_New(t *testing.T) {
 
 func TestResourceStateCollector_ProcessStatusEvent(t *testing.T) {
 	testCases := map[string]struct {
-		resourceGroups []event.ResourceGroup
+		resourceGroups []event.ActionGroup
 		statusEvent    event.StatusEvent
 	}{
 		"nil StatusEvent.Resource does not crash": {
-			resourceGroups: []event.ResourceGroup{},
+			resourceGroups: []event.ActionGroup{},
 			statusEvent: event.StatusEvent{
-				Type:     event.StatusEventResourceUpdate,
-				Resource: nil,
-			},
-		},
-		"type StatusEventCompleted does nothing": {
-			resourceGroups: []event.ResourceGroup{},
-			statusEvent: event.StatusEvent{
-				Type:     event.StatusEventCompleted,
 				Resource: nil,
 			},
 		},
 		"unfound Resource identifier does not crash": {
-			resourceGroups: []event.ResourceGroup{
+			resourceGroups: []event.ActionGroup{
 				{
 					Action:      event.ApplyAction,
 					Identifiers: []object.ObjMetadata{depID},
 				},
 			},
 			statusEvent: event.StatusEvent{
-				Type: event.StatusEventResourceUpdate,
-				Resource: &pe.ResourceStatus{
+				PollResourceInfo: &pe.ResourceStatus{
 					Identifier: customID, // Does not match identifier in resourceGroups
 				},
 			},
 		},
 		"basic status event for applying two resources updates resourceStatus": {
-			resourceGroups: []event.ResourceGroup{
+			resourceGroups: []event.ActionGroup{
 				{
 					Action: event.ApplyAction,
 					Identifiers: []object.ObjMetadata{
@@ -145,15 +136,14 @@ func TestResourceStateCollector_ProcessStatusEvent(t *testing.T) {
 				},
 			},
 			statusEvent: event.StatusEvent{
-				Type: event.StatusEventResourceUpdate,
-				Resource: &pe.ResourceStatus{
+				PollResourceInfo: &pe.ResourceStatus{
 					Identifier: depID,
 					Message:    testMessage,
 				},
 			},
 		},
 		"several resources for prune": {
-			resourceGroups: []event.ResourceGroup{
+			resourceGroups: []event.ActionGroup{
 				{
 					Action: event.ApplyAction,
 					Identifiers: []object.ObjMetadata{
@@ -168,8 +158,7 @@ func TestResourceStateCollector_ProcessStatusEvent(t *testing.T) {
 				},
 			},
 			statusEvent: event.StatusEvent{
-				Type: event.StatusEventResourceUpdate,
-				Resource: &pe.ResourceStatus{
+				PollResourceInfo: &pe.ResourceStatus{
 					Identifier: depID,
 					Message:    testMessage,
 				},
@@ -186,7 +175,7 @@ func TestResourceStateCollector_ProcessStatusEvent(t *testing.T) {
 				resourceInfo, found := rsc.resourceInfos[id]
 				if found {
 					// Validate the ResourceStatus was set from StatusEvent
-					if resourceInfo.resourceStatus != tc.statusEvent.Resource {
+					if resourceInfo.resourceStatus != tc.statusEvent.PollResourceInfo {
 						t.Errorf("status event not processed for %s", id)
 					}
 				}
@@ -199,5 +188,5 @@ func getID(e event.StatusEvent) (object.ObjMetadata, bool) {
 	if e.Resource == nil {
 		return object.ObjMetadata{}, false
 	}
-	return e.Resource.Identifier, true
+	return e.Identifier, true
 }
diff --git a/thirdparty/cli-utils/printers/table/printer_test.go b/thirdparty/cli-utils/printers/table/printer_test.go
index ca6a4f2e..7af0b5b0 100644
--- a/thirdparty/cli-utils/printers/table/printer_test.go
+++ b/thirdparty/cli-utils/printers/table/printer_test.go
@@ -35,7 +35,7 @@ func TestActionColumnDef(t *testing.T) {
 		"applied": {
 			resource: &ResourceInfo{
 				ResourceAction: event.ApplyAction,
-				ApplyOpResult:  &createdOpResult,
+				ApplyOpResult:  createdOpResult,
 			},
 			columnWidth:    15,
 			expectedOutput: "Created",
@@ -43,7 +43,7 @@ func TestActionColumnDef(t *testing.T) {
 		"pruned": {
 			resource: &ResourceInfo{
 				ResourceAction: event.PruneAction,
-				PruneOpResult:  &prunedOpResult,
+				PruneOpResult:  prunedOpResult,
 			},
 			columnWidth:    15,
 			expectedOutput: "Pruned",
@@ -51,7 +51,7 @@ func TestActionColumnDef(t *testing.T) {
 		"trimmed output": {
 			resource: &ResourceInfo{
 				ResourceAction: event.ApplyAction,
-				ApplyOpResult:  &createdOpResult,
+				ApplyOpResult:  createdOpResult,
 			},
 			columnWidth:    5,
 			expectedOutput: "Creat",