Repository: GoogleContainerTools/kpt
Pull request #2079 (2021-05-21 19:14:49)

add test for env in eval
The test case will build a simple image to print the envs.



Patch:
diff --git a/.gitignore b/.gitignore
index a5fb80b0..b8f4fbc4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,6 +26,9 @@ kpt/kpt
 # VS Code individual configurations
 .vscode/
 
+# Firebase cache
+.firebase/
+
 # Node dependencies
 node_modules/
 package-lock.json
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/commands/livecmd.go b/commands/livecmd.go
index 407cb179..8cfa58b7 100644
--- a/commands/livecmd.go
+++ b/commands/livecmd.go
@@ -62,12 +62,6 @@ func GetLiveCommand(ctx context.Context, _, version string) *cobra.Command {
 
 	f := newFactory(liveCmd, version)
 
-	// The provider handles both ConfigMap and ResourceGroup inventory objects.
-	// If a package has both inventory objects, then an error is thrown.
-	klog.V(2).Infoln("provider supports ResourceGroup and ConfigMap inventory")
-	dp := live.NewDualDelegatingProvider(f)
-	dl := live.NewDualDelegatingManifestReader(f)
-
 	rgProvider := live.NewResourceGroupProvider(f)
 	rgLoader := live.NewResourceGroupManifestLoader(f)
 
@@ -79,11 +73,7 @@ func GetLiveCommand(ctx context.Context, _, version string) *cobra.Command {
 	initCmd := cmdliveinit.NewCommand(ctx, f, ioStreams)
 	applyCmd := cmdapply.NewCommand(ctx, rgProvider, rgLoader, ioStreams)
 	destroyCmd := cmddestroy.NewCommand(ctx, rgProvider, rgLoader, ioStreams)
-
-	statusCmd := status.GetStatusRunner(dp, dl).Command
-	statusCmd.Short = livedocs.StatusShort
-	statusCmd.Long = livedocs.StatusLong
-	statusCmd.Example = livedocs.StatusExamples
+	statusCmd := status.NewCommand(ctx, rgProvider, rgLoader, ioStreams)
 
 	liveCmd.AddCommand(initCmd, applyCmd, destroyCmd, statusCmd)
 
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 5f81e04f..017764f5 100644
--- a/internal/cmdliveinit/cmdliveinit.go
+++ b/internal/cmdliveinit/cmdliveinit.go
@@ -12,6 +12,8 @@ import (
 	"strings"
 	"time"
 
+	"github.com/GoogleContainerTools/kpt/internal/errors"
+	"github.com/GoogleContainerTools/kpt/internal/pkg"
 	"github.com/GoogleContainerTools/kpt/internal/printer"
 	kptfilev1alpha2 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1alpha2"
 	"github.com/GoogleContainerTools/kpt/pkg/kptfile/kptfileutil"
@@ -67,7 +69,6 @@ type Runner struct {
 
 	factory     cmdutil.Factory
 	ioStreams   genericclioptions.IOStreams
-	dir         string // Directory with Kptfile
 	Force       bool   // Set inventory values even if already set in Kptfile
 	Name        string // Inventory object name
 	namespace   string // Inventory object namespace
@@ -76,35 +77,44 @@ type Runner struct {
 }
 
 func (r *Runner) runE(_ *cobra.Command, args []string) error {
+	const op errors.Op = "cmdliveinit.runE"
 	if len(args) == 0 {
 		// default to the current working directory
 		cwd, err := os.Getwd()
 		if err != nil {
-			return err
+			return errors.E(op, err)
 		}
 		args = append(args, cwd)
 	}
 
 	dir, err := config.NormalizeDir(args[0])
 	if err != nil {
-		return err
+		return errors.E(op, err)
 	}
-	r.dir = dir
 
-	return (&ConfigureInventoryInfo{
-		Path:        dir,
+	p, err := pkg.New(dir)
+	if err != nil {
+		return errors.E(op, err)
+	}
+
+	err = (&ConfigureInventoryInfo{
+		Pkg:         p,
 		Factory:     r.factory,
 		Quiet:       r.Quiet,
 		Name:        r.Name,
 		InventoryID: r.InventoryID,
 		Force:       r.Force,
 	}).Run(r.ctx)
+	if err != nil {
+		return errors.E(op, p.UniquePath, err)
+	}
+	return nil
 }
 
 // ConfigureInventoryInfo contains the functionality for adding and updating
 // the inventory information in the Kptfile.
 type ConfigureInventoryInfo struct {
-	Path    string
+	Pkg     *pkg.Pkg
 	Factory cmdutil.Factory
 	Quiet   bool
 
@@ -116,13 +126,14 @@ type ConfigureInventoryInfo struct {
 
 // Run updates the inventory info in the package given by the Path.
 func (c *ConfigureInventoryInfo) Run(ctx context.Context) error {
+	const op errors.Op = "cmdliveinit.Run"
 	pr := printer.FromContextOrDie(ctx)
 
 	var name, namespace, inventoryID string
 
-	ns, err := config.FindNamespace(c.Factory.ToRawKubeConfigLoader(), c.Path)
+	ns, err := config.FindNamespace(c.Factory.ToRawKubeConfigLoader(), c.Pkg.UniquePath.String())
 	if err != nil {
-		return err
+		return errors.E(op, c.Pkg.UniquePath, err)
 	}
 	namespace = strings.TrimSpace(ns)
 	if !c.Quiet {
@@ -131,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
@@ -140,48 +151,52 @@ func (c *ConfigureInventoryInfo) Run(ctx context.Context) error {
 	if c.InventoryID == "" {
 		id, err := generateID(namespace, name, time.Now())
 		if err != nil {
-			return err
+			return errors.E(op, c.Pkg.UniquePath, err)
 		}
 		inventoryID = id
 	} else {
 		inventoryID = c.InventoryID
 	}
 	// Finally, update these values in the Inventory section of the Kptfile.
-	err = updateKptfile(c.Path, &kptfilev1alpha2.Inventory{
+	err = updateKptfile(c.Pkg, &kptfilev1alpha2.Inventory{
 		Namespace:   namespace,
 		Name:        name,
 		InventoryID: inventoryID,
 	}, c.Force)
 	if !c.Quiet {
 		if err == nil {
-			pr.Printf("success")
+			pr.Printf("success\n")
 		} else {
-			pr.Printf("failed")
+			pr.Printf("failed\n")
 		}
 	}
-	return err
+	if err != nil {
+		return errors.E(op, c.Pkg.UniquePath, err)
+	}
+	return nil
 }
 
 // Run fills in the inventory object values into the Kptfile.
-func updateKptfile(path string, inv *kptfilev1alpha2.Inventory, force bool) error {
+func updateKptfile(p *pkg.Pkg, inv *kptfilev1alpha2.Inventory, force bool) error {
+	const op errors.Op = "cmdliveinit.updateKptfile"
 	// Read the Kptfile io io.dir
-	kf, err := kptfileutil.ReadFile(path)
+	kf, err := p.Kptfile()
 	if err != nil {
-		return err
+		return errors.E(op, p.UniquePath, err)
 	}
 	// Validate the inventory values don't already exist
 	isEmpty := kptfileInventoryEmpty(kf.Inventory)
 	if !isEmpty && !force {
-		return &InvExistsError{}
+		return errors.E(op, p.UniquePath, &InvExistsError{})
 	}
 	// Check the new inventory values are valid.
 	if err := inv.Validate(); err != nil {
-		return err
+		return errors.E(op, p.UniquePath, err)
 	}
 	// Finally, set the inventory parameters in the Kptfile and write it.
 	kf.Inventory = inv
-	if err := kptfileutil.WriteFile(path, kf); err != nil {
-		return err
+	if err := kptfileutil.WriteFile(p.UniquePath.String(), *kf); err != nil {
+		return errors.E(op, p.UniquePath, err)
 	}
 	return nil
 }
@@ -190,9 +205,10 @@ func updateKptfile(path string, inv *kptfilev1alpha2.Inventory, force bool) erro
 // and name, with the unix timestamp string concatenated. Returns an error
 // if either the namespace or name are empty.
 func generateID(namespace string, name string, t time.Time) (string, error) {
+	const op errors.Op = "cmdliveinit.generateID"
 	hashStr, err := generateHash(namespace, name)
 	if err != nil {
-		return "", err
+		return "", errors.E(op, err)
 	}
 	timeStr := strconv.FormatInt(t.UTC().UnixNano(), 10)
 	return fmt.Sprintf("%s-%s", hashStr, timeStr), nil
@@ -201,13 +217,15 @@ func generateID(namespace string, name string, t time.Time) (string, error) {
 // generateHash returns the SHA1 hash of the concatenated "namespace:name" string,
 // or an error if either namespace or name is empty.
 func generateHash(namespace string, name string) (string, error) {
+	const op errors.Op = "cmdliveinit.generateHash"
 	if len(namespace) == 0 || len(name) == 0 {
-		return "", fmt.Errorf("can not generate hash with empty namespace or name")
+		return "", errors.E(op,
+			fmt.Errorf("can not generate hash with empty namespace or name"))
 	}
 	str := fmt.Sprintf("%s:%s", namespace, name)
 	h := sha1.New()
 	if _, err := h.Write([]byte(str)); err != nil {
-		return "", err
+		return "", errors.E(op, err)
 	}
 	return fmt.Sprintf("%x", (h.Sum(nil))), nil
 }
diff --git a/internal/cmdmigrate/migratecmd.go b/internal/cmdmigrate/migratecmd.go
index d43e88f5..1c3d8f9c 100644
--- a/internal/cmdmigrate/migratecmd.go
+++ b/internal/cmdmigrate/migratecmd.go
@@ -13,6 +13,7 @@ import (
 	"os"
 
 	"github.com/GoogleContainerTools/kpt/internal/cmdliveinit"
+	"github.com/GoogleContainerTools/kpt/internal/pkg"
 	"github.com/GoogleContainerTools/kpt/pkg/live"
 	"github.com/spf13/cobra"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -180,8 +181,12 @@ func (mr *MigrateRunner) applyCRD() error {
 func (mr *MigrateRunner) updateKptfile(ctx context.Context, args []string, prevID string) error {
 	fmt.Fprint(mr.ioStreams.Out, "  updating Kptfile inventory values...")
 	if !mr.dryRun {
-		err := (&cmdliveinit.ConfigureInventoryInfo{
-			Path:        args[0],
+		p, err := pkg.New(args[0])
+		if err != nil {
+			return err
+		}
+		err = (&cmdliveinit.ConfigureInventoryInfo{
+			Pkg:         p,
 			Factory:     mr.rgProvider.Factory(),
 			Quiet:       true,
 			InventoryID: prevID,
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/internal/util/fetch/fetch.go b/internal/util/fetch/fetch.go
index 664f6101..bbcb08cf 100644
--- a/internal/util/fetch/fetch.go
+++ b/internal/util/fetch/fetch.go
@@ -140,9 +140,15 @@ func ClonerUsingGitExec(ctx context.Context, repoSpec *git.RepoSpec) error {
 
 	// Check if we have a ref in the upstream that matches the package-specific
 	// reference. If we do, we use that reference.
-	packageRef := path.Join(strings.TrimLeft(repoSpec.Path, "/"), repoSpec.Ref)
-	if _, found := upstreamRepo.ResolveTag(packageRef); found {
-		repoSpec.Ref = packageRef
+	ps := strings.Split(repoSpec.Path, "/")
+	for len(ps) != 0 {
+		p := path.Join(ps...)
+		packageRef := path.Join(strings.TrimLeft(p, "/"), repoSpec.Ref)
+		if _, found := upstreamRepo.ResolveTag(packageRef); found {
+			repoSpec.Ref = packageRef
+			break
+		}
+		ps = ps[:len(ps)-1]
 	}
 
 	// Pull the required ref into the repo git cache.
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/pkg/live/rgloader.go b/pkg/live/rgloader.go
index 067a87fa..101493ea 100644
--- a/pkg/live/rgloader.go
+++ b/pkg/live/rgloader.go
@@ -4,7 +4,6 @@
 package live
 
 import (
-	"fmt"
 	"io"
 
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -15,6 +14,12 @@ import (
 
 var _ manifestreader.ManifestLoader = &ResourceGroupManifestLoader{}
 
+type NoInvInfoError struct{}
+
+func (e *NoInvInfoError) Error() string {
+	return "inventory info not found in Kptfile"
+}
+
 // ResourceGroupManifestLoader implements the Provider interface, returning
 // ResourceGroup versions of some kpt live apply structures.
 type ResourceGroupManifestLoader struct {
@@ -32,7 +37,7 @@ func NewResourceGroupManifestLoader(f util.Factory) *ResourceGroupManifestLoader
 func (f *ResourceGroupManifestLoader) InventoryInfo(objs []*unstructured.Unstructured) (inventory.InventoryInfo, []*unstructured.Unstructured, error) {
 	objs, invObj := findResourceGroupInv(objs)
 	if invObj == nil {
-		return nil, nil, fmt.Errorf("unable to find the ResourceGroup inventory info")
+		return nil, nil, &NoInvInfoError{}
 	}
 	return &InventoryResourceGroup{inv: invObj}, objs, nil
 }
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/site/index.html b/site/index.html
index f5169c0b..0e78f95d 100644
--- a/site/index.html
+++ b/site/index.html
@@ -10,6 +10,10 @@
       name="viewport"
       content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
     />
+    <link
+      rel="stylesheet"
+      href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css"
+    />
     <link
       rel="stylesheet"
       href="https://cdn.jsdelivr.net/npm/docsify-themeable@0/dist/css/theme-simple.css"
@@ -39,6 +43,8 @@
     <script src="//cdn.jsdelivr.net/npm/prismjs@1.22/components/prism-go.min.js"></script>
     <script src="//cdn.jsdelivr.net/npm/prismjs@1.22/components/prism-typescript.min.js"></script>
     <script src="//cdn.jsdelivr.net/npm/@gouch/to-title-case@2.2.1/to-title-case.min.js"></script>
+    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
+    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
     <!--Fallback content on 404s can go here.-->
   </body>
 </html>
diff --git a/site/static/css/site.css b/site/static/css/site.css
index 3ac00a2d..fc667bdb 100644
--- a/site/static/css/site.css
+++ b/site/static/css/site.css
@@ -19,18 +19,59 @@
   --pagination-title-font-size: 0px;
 }
 
+.close,
+.close:focus,
+.close:hover {
+  float: unset;
+  font-size: unset;
+  font-weight: unset;
+  line-height: unset;
+  color: unset;
+  text-shadow: unset;
+  filter: unset;
+  opacity: unset;
+}
+
 .cover-main h1 img {
   height: 72px;
 }
 
+.cover-main blockquote {
+  border-left: unset;
+}
+
+.cover.show {
+  display: flex !important;
+}
+
+body {
+  background-color: unset;
+  line-height: unset;
+  font-family: unset;
+  font-size: unset;
+}
+h1 {
+  line-height: unset;
+}
+
+.dropdown-toggle {
+  background: var(--theme-color);
+  border: none;
+}
+
 .sidebar {
   color: rgba(255, 255, 255, 0.75);
+  font-size: 16px;
 }
 
 .sidebar-toggle {
   bottom: 0;
 }
 
+.sidebar > h1 {
+  font-size: var(--sidebar-name-font-size);
+}
+
 .sidebar > h1 img {
   height: 32px;
 }
diff --git a/site/static/js/config.js b/site/static/js/config.js
index c6c8e7c6..19c29165 100644
--- a/site/static/js/config.js
+++ b/site/static/js/config.js
@@ -11,7 +11,10 @@ window.$docsify = {
     "/*.*/sidebar.md": "/sidebar.md",
   },
   coverpage: "coverpage.md",
-  crossOriginLinks:["https://catalog.kpt.dev/"],
+  crossOriginLinks: [
+    "https://catalog.kpt.dev/",
+    "https://googlecontainertools.github.io/kpt/installation/",
+  ],
   auto2top: true,
   repo: "true",
   pagination: {
diff --git a/site/static/js/plugins.js b/site/static/js/plugins.js
index 4ffdd50e..b466c753 100644
--- a/site/static/js/plugins.js
+++ b/site/static/js/plugins.js
@@ -43,6 +43,24 @@ function convertFromHugo(content) {
   return content.replace(hugoHideDirectives, "").replace(hugoDirectiveTags, "");
 }
 
+async function addVersionDropdown() {
+  const sidebar = document.getElementsByClassName("sidebar").item(0);
+  const latestVersion = "v1.0.0-alpha";
+  const versionDropdown = `
+  <div class="dropdown">
+                <button class="btn btn-primary dropdown-toggle" type="button" data-toggle="dropdown">${latestVersion}
+                <span class="caret"></span></button>
+                <ol class="dropdown-menu">
+                <li><a href="/installation/">${latestVersion}</a></li>
+                <li><a href="https://googlecontainertools.github.io/kpt/installation/" target="_self">v0.39</a></li>
+                </ol>
+              </div>
+                `;
+  const node = document.createElement("div");
+  node.innerHTML = versionDropdown;
+  sidebar.getElementsByClassName("app-name").item(0).appendChild(node);
+}
+
 function showBookPageFooters() {
   const paginationFooters = Array.from(
     document.getElementsByClassName("docsify-pagination-container")
@@ -162,6 +180,8 @@ function localPlugins(hook, _vm) {
   // Add title to book Markdown pages based on directory structure.
   hook.beforeEach(processBookPageTitle);
 
+  hook.mounted(addVersionDropdown);
+
   // Show navigation footer for book pages.
   hook.doneEach(showBookPageFooters);
 
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 71505afa..d332949a 100644
--- a/thirdparty/cli-utils/status/cmdstatus.go
+++ b/thirdparty/cli-utils/status/cmdstatus.go
@@ -6,14 +6,17 @@ package status
 import (
 	"context"
 	"fmt"
+	"os"
 	"time"
 
-	"github.com/GoogleContainerTools/kpt/thirdparty/cli-utils/flagutils"
+	"github.com/GoogleContainerTools/kpt/internal/cmdutil"
+	"github.com/GoogleContainerTools/kpt/internal/docs/generated/livedocs"
 	"github.com/GoogleContainerTools/kpt/thirdparty/cli-utils/status/printers"
 	"github.com/go-errors/errors"
 	"github.com/spf13/cobra"
 	"k8s.io/cli-runtime/pkg/genericclioptions"
-	cmdutil "k8s.io/kubectl/pkg/cmd/util"
+	"k8s.io/kubectl/pkg/cmd/util"
+	"k8s.io/kubectl/pkg/util/slice"
 	"sigs.k8s.io/cli-utils/pkg/apply/poller"
 	"sigs.k8s.io/cli-utils/pkg/common"
 	"sigs.k8s.io/cli-utils/pkg/kstatus/polling"
@@ -26,63 +29,94 @@ import (
 	"sigs.k8s.io/cli-utils/pkg/util/factory"
 )
 
-func GetStatusRunner(provider provider.Provider, loader manifestreader.ManifestLoader) *StatusRunner {
-	r := &StatusRunner{
+const (
+	Known   = "known"
+	Current = "current"
+	Deleted = "deleted"
+	Forever = "forever"
+)
+
+var (
+	PollUntilOptions = []string{Known, Current, Deleted, Forever}
+)
+
+func NewRunner(ctx context.Context, provider provider.Provider, loader manifestreader.ManifestLoader,
+	ioStreams genericclioptions.IOStreams) *Runner {
+	r := &Runner{
+		ctx:               ctx,
 		provider:          provider,
 		loader:            loader,
 		pollerFactoryFunc: pollerFactoryFunc,
+		ioStreams:         ioStreams,
 	}
 	c := &cobra.Command{
-		Use:  "status [PKG_PATH | -]",
-		RunE: r.runE,
+		Use:     "status [PKG_PATH | -]",
+		PreRunE: r.preRunE,
+		RunE:    r.runE,
+		Short:   livedocs.StatusShort,
+		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",
-		"When to stop polling. Must be one of 'known', 'current', 'deleted', or 'forever'.")
+		fmt.Sprintf("When to stop polling. Must be one of %s", cmdutil.JoinStringsWithQuotes(PollUntilOptions)))
 	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
 }
 
-func StatusCommand(f cmdutil.Factory) *cobra.Command {
-	provider := provider.NewProvider(f)
-	loader := manifestreader.NewManifestLoader(f)
-	return GetStatusRunner(provider, loader).Command
+func NewCommand(ctx context.Context, provider provider.Provider,
+	loader manifestreader.ManifestLoader, ioStreams genericclioptions.IOStreams) *cobra.Command {
+	return NewRunner(ctx, provider, loader, ioStreams).Command
 }
 
-// StatusRunner captures the parameters for the command and contains
+// Runner captures the parameters for the command and contains
 // the run function.
-type StatusRunner struct {
-	Command  *cobra.Command
-	provider provider.Provider
-	loader   manifestreader.ManifestLoader
+type Runner struct {
+	ctx       context.Context
+	Command   *cobra.Command
+	ioStreams genericclioptions.IOStreams
+	provider  provider.Provider
+	loader    manifestreader.ManifestLoader
 
 	period    time.Duration
 	pollUntil string
 	timeout   time.Duration
 	output    string
 
-	pollerFactoryFunc func(cmdutil.Factory) (poller.Poller, error)
+	pollerFactoryFunc func(util.Factory) (poller.Poller, error)
+}
+
+func (r *Runner) preRunE(*cobra.Command, []string) error {
+	if !slice.ContainsString(PollUntilOptions, r.pollUntil, nil) {
+		return fmt.Errorf("pollUntil must be one of %s",
+			cmdutil.JoinStringsWithQuotes(PollUntilOptions))
+	}
+	return nil
 }
 
 // runE implements the logic of the command and will delegate to the
 // poller to compute status for each of the resources. One of the printer
 // implementations takes care of printing the output.
-func (r *StatusRunner) runE(cmd *cobra.Command, args []string) error {
+func (r *Runner) runE(cmd *cobra.Command, args []string) error {
 	if len(args) == 0 {
 		// default to the current working directory
-		args = append(args, ".")
+		cwd, err := os.Getwd()
+		if err != nil {
+			return err
+		}
+		args = append(args, cwd)
 	}
+
 	_, err := common.DemandOneDirectory(args)
 	if err != nil {
 		return err
 	}
 
-	reader, err := r.loader.ManifestReader(cmd.InOrStdin(), flagutils.PathFromArgs(args))
+	reader, err := r.loader.ManifestReader(cmd.InOrStdin(), args[0])
 	if err != nil {
 		return err
 	}
@@ -110,7 +144,7 @@ func (r *StatusRunner) runE(cmd *cobra.Command, args []string) error {
 
 	// Exit here if the inventory is empty.
 	if len(identifiers) == 0 {
-		_, _ = fmt.Fprint(cmd.OutOrStdout(), "no resources found in the inventory\n")
+		_, _ = fmt.Fprint(r.ioStreams.Out, "no resources found in the inventory\n")
 		return nil
 	}
 
@@ -121,23 +155,19 @@ func (r *StatusRunner) runE(cmd *cobra.Command, args []string) error {
 
 	// Fetch a printer implementation based on the desired output format as
 	// specified in the output flag.
-	printer, err := printers.CreatePrinter(r.output, genericclioptions.IOStreams{
-		In:     cmd.InOrStdin(),
-		Out:    cmd.OutOrStdout(),
-		ErrOut: cmd.ErrOrStderr(),
-	})
+	printer, err := printers.CreatePrinter(r.output, r.ioStreams)
 	if err != nil {
 		return errors.WrapPrefix(err, "error creating printer", 1)
 	}
 
 	// If the user has specified a timeout, we create a context with timeout,
 	// otherwise we create a context with cancel.
-	ctx := context.Background()
+	var ctx context.Context
 	var cancel func()
 	if r.timeout != 0 {
-		ctx, cancel = context.WithTimeout(ctx, r.timeout)
+		ctx, cancel = context.WithTimeout(r.ctx, r.timeout)
 	} else {
-		ctx, cancel = context.WithCancel(ctx)
+		ctx, cancel = context.WithCancel(r.ctx)
 	}
 	defer cancel()
 
@@ -145,13 +175,13 @@ func (r *StatusRunner) runE(cmd *cobra.Command, args []string) error {
 	// the command should exit.
 	var cancelFunc collector.ObserverFunc
 	switch r.pollUntil {
-	case "known":
+	case Known:
 		cancelFunc = allKnownNotifierFunc(cancel)
-	case "current":
+	case Current:
 		cancelFunc = desiredStatusNotifierFunc(cancel, status.CurrentStatus)
-	case "deleted":
+	case Deleted:
 		cancelFunc = desiredStatusNotifierFunc(cancel, status.NotFoundStatus)
-	case "forever":
+	case Forever:
 		cancelFunc = func(*collector.ResourceStatusCollector, event.Event) {}
 	default:
 		return fmt.Errorf("unknown value for pollUntil: %q", r.pollUntil)
@@ -197,6 +227,6 @@ func allKnownNotifierFunc(cancelFunc context.CancelFunc) collector.ObserverFunc
 	}
 }
 
-func pollerFactoryFunc(f cmdutil.Factory) (poller.Poller, error) {
+func pollerFactoryFunc(f util.Factory) (poller.Poller, error) {
 	return factory.NewStatusPoller(f)
 }
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/function-env/.expected/config.yaml b/e2e/testdata/fn-eval/function-env/.expected/config.yaml
new file mode 100644
index 00000000..0678c588
--- /dev/null
+++ b/e2e/testdata/fn-eval/function-env/.expected/config.yaml
@@ -0,0 +1,16 @@
+# 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: "FOO=BAR EXPORT_ENV=export_env_value"
diff --git a/e2e/testdata/fn-eval/function-env/.expected/exec.sh b/e2e/testdata/fn-eval/function-env/.expected/exec.sh
new file mode 100644
index 00000000..37edc9f1
--- /dev/null
+++ b/e2e/testdata/fn-eval/function-env/.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
+
+IMAGE_TAG="gcr.io/kpt-fn-demo/printenv:v0.1"
+export EXPORT_ENV="export_env_value"
+
+kpt fn source \
+| kpt fn eval - --image $IMAGE_TAG -e EXPORT_ENV -e FOO=BAR
diff --git a/e2e/testdata/fn-eval/function-env/.krmginore b/e2e/testdata/fn-eval/function-env/.krmginore
new file mode 100644
index 00000000..9d7a4007
--- /dev/null
+++ b/e2e/testdata/fn-eval/function-env/.krmginore
@@ -0,0 +1,1 @@
+.expected
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/output-to-stdout/.expected/exec.sh b/e2e/testdata/fn-eval/output-to-stdout/.expected/exec.sh
index 7a989309..a662d1e3 100644
--- a/e2e/testdata/fn-eval/output-to-stdout/.expected/exec.sh
+++ b/e2e/testdata/fn-eval/output-to-stdout/.expected/exec.sh
@@ -1,4 +1,19 @@
 #! /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 source |\
 kpt fn eval - --image gcr.io/kpt-fn/set-namespace:v0.1 -- namespace=staging
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/cmdliveinit/cmdliveinit_test.go b/internal/cmdliveinit/cmdliveinit_test.go
index 0e155509..ea5f920c 100644
--- a/internal/cmdliveinit/cmdliveinit_test.go
+++ b/internal/cmdliveinit/cmdliveinit_test.go
@@ -105,6 +105,28 @@ func TestCmd_generateID(t *testing.T) {
 	}
 }
 
+func TestCmd_Run_NoKptfile(t *testing.T) {
+	// Set up fake test factory
+	tf := cmdtesting.NewTestFactory().WithNamespace("test-ns")
+	defer tf.Cleanup()
+	ioStreams, _, _, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled
+
+	w, clean := testutil.SetupWorkspace(t)
+	defer clean()
+
+	revert := testutil.Chdir(t, w.WorkspaceDirectory)
+	defer revert()
+
+	runner := NewRunner(fake.CtxWithNilPrinter(), tf, ioStreams)
+	runner.Command.SetArgs([]string{})
+	err := runner.Command.Execute()
+
+	if !assert.Error(t, err) {
+		t.FailNow()
+	}
+	assert.Contains(t, err.Error(), "error reading Kptfile at")
+}
+
 func TestCmd_Run(t *testing.T) {
 	testCases := map[string]struct {
 		kptfile           string
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/internal/util/fetch/fetch_test.go b/internal/util/fetch/fetch_test.go
index 6292e542..25c70130 100644
--- a/internal/util/fetch/fetch_test.go
+++ b/internal/util/fetch/fetch_test.go
@@ -19,9 +19,10 @@ import (
 	"path/filepath"
 	"testing"
 
-	"github.com/GoogleContainerTools/kpt/internal/pkg"
+	pkgtesting "github.com/GoogleContainerTools/kpt/internal/pkg/testing"
 	"github.com/GoogleContainerTools/kpt/internal/printer/fake"
 	"github.com/GoogleContainerTools/kpt/internal/testutil"
+	"github.com/GoogleContainerTools/kpt/internal/testutil/pkgbuilder"
 	. "github.com/GoogleContainerTools/kpt/internal/util/fetch"
 	kptfilev1alpha2 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1alpha2"
 	"github.com/GoogleContainerTools/kpt/pkg/kptfile/kptfileutil"
@@ -57,12 +58,15 @@ func createKptfile(workspace *testutil.TestWorkspace, git *kptfilev1alpha2.Git,
 	return kptfileutil.WriteFile(workspace.FullPackagePath(), kf)
 }
 
-func createPackage(t *testing.T, pkgPath string) *pkg.Pkg {
-	p, err := pkg.New(pkgPath)
-	if !assert.NoError(t, err) {
-		t.FailNow()
+func setKptfileName(workspace *testutil.TestWorkspace, name string) error {
+	kf, err := kptfileutil.ReadFile(workspace.FullPackagePath())
+	if err != nil {
+		return err
 	}
-	return p
+
+	kf.Name = name
+	err = kptfileutil.WriteFile(workspace.FullPackagePath(), kf)
+	return err
 }
 
 // TestCommand_Run_failEmptyRepo verifies that Command fail if no Kptfile
@@ -80,7 +84,7 @@ func TestCommand_Run_failNoKptfile(t *testing.T) {
 	}
 
 	err = Command{
-		Pkg: createPackage(t, pkgPath),
+		Pkg: pkgtesting.CreatePkgOrFail(t, pkgPath),
 	}.Run(fake.CtxWithNilPrinter())
 	if !assert.Error(t, err) {
 		t.FailNow()
@@ -99,7 +103,7 @@ func TestCommand_Run_failNoGit(t *testing.T) {
 	}
 
 	err = Command{
-		Pkg: createPackage(t, w.FullPackagePath()),
+		Pkg: pkgtesting.CreatePkgOrFail(t, w.FullPackagePath()),
 	}.Run(fake.CtxWithNilPrinter())
 	if !assert.Error(t, err) {
 		t.FailNow()
@@ -122,7 +126,7 @@ func TestCommand_Run_failEmptyRepo(t *testing.T) {
 	}
 
 	err = Command{
-		Pkg: createPackage(t, w.FullPackagePath()),
+		Pkg: pkgtesting.CreatePkgOrFail(t, w.FullPackagePath()),
 	}.Run(fake.CtxWithNilPrinter())
 	if !assert.Error(t, err) {
 		t.FailNow()
@@ -145,7 +149,7 @@ func TestCommand_Run_failNoRevision(t *testing.T) {
 	}
 
 	err = Command{
-		Pkg: createPackage(t, w.FullPackagePath()),
+		Pkg: pkgtesting.CreatePkgOrFail(t, w.FullPackagePath()),
 	}.Run(fake.CtxWithNilPrinter())
 	if !assert.Error(t, err) {
 		t.FailNow()
@@ -172,7 +176,7 @@ func TestCommand_Run(t *testing.T) {
 
 	absPath := filepath.Join(w.WorkspaceDirectory, g.RepoName)
 	err = Command{
-		Pkg: createPackage(t, w.FullPackagePath()),
+		Pkg: pkgtesting.CreatePkgOrFail(t, w.FullPackagePath()),
 	}.Run(fake.CtxWithNilPrinter())
 	assert.NoError(t, err)
 
@@ -234,7 +238,7 @@ func TestCommand_Run_subdir(t *testing.T) {
 
 	absPath := filepath.Join(w.WorkspaceDirectory, g.RepoName)
 	err = Command{
-		Pkg: createPackage(t, w.FullPackagePath()),
+		Pkg: pkgtesting.CreatePkgOrFail(t, w.FullPackagePath()),
 	}.Run(fake.CtxWithNilPrinter())
 	assert.NoError(t, err)
 
@@ -312,7 +316,7 @@ func TestCommand_Run_branch(t *testing.T) {
 	}
 
 	err = Command{
-		Pkg: createPackage(t, w.FullPackagePath()),
+		Pkg: pkgtesting.CreatePkgOrFail(t, w.FullPackagePath()),
 	}.Run(fake.CtxWithNilPrinter())
 	assert.NoError(t, err)
 
@@ -393,7 +397,7 @@ func TestCommand_Run_tag(t *testing.T) {
 	}
 
 	err = Command{
-		Pkg: createPackage(t, w.FullPackagePath()),
+		Pkg: pkgtesting.CreatePkgOrFail(t, w.FullPackagePath()),
 	}.Run(fake.CtxWithNilPrinter())
 	assert.NoError(t, err)
 
@@ -433,6 +437,187 @@ func TestCommand_Run_tag(t *testing.T) {
 	})
 }
 
+func TestCommand_Run_subdir_at_tag(t *testing.T) {
+	testCases := map[string]struct {
+		dir         string
+		tag         string
+		upstreamPkg *pkgbuilder.RootPkg
+	}{
+		"reads subdirectory": {
+			dir: "/java/expected",
+			tag: "java/v2",
+			upstreamPkg: pkgbuilder.NewRootPkg().
+				WithSubPackages(pkgbuilder.NewSubPkg("java").
+					WithResource("deployment").
+					WithSubPackages(pkgbuilder.NewSubPkg("expected").
+						WithFile("expected.txt", "My kptfile and I should be the only objects"))),
+		},
+		"reads subdirectory with no leading slash": {
+			dir: "java/expected",
+			tag: "java/v2",
+			upstreamPkg: pkgbuilder.NewRootPkg().
+				WithSubPackages(pkgbuilder.NewSubPkg("java").
+					WithResource("deployment").
+					WithSubPackages(pkgbuilder.NewSubPkg("expected").
+						WithFile("expected.txt", "My kptfile and I should be the only objects"))),
+		},
+		"reads specific subdirectory": {
+			dir: "/java/not_expected/java/expected",
+			tag: "java/expected/v2",
+			upstreamPkg: pkgbuilder.NewRootPkg().
+				WithSubPackages(pkgbuilder.NewSubPkg("java").
+					WithResource("deployment").
+					WithSubPackages(pkgbuilder.NewSubPkg("not_expected").
+						WithFile("not_actually_expected.txt", "I should not be present").
+						WithSubPackages(pkgbuilder.NewSubPkg("java").
+							WithSubPackages(pkgbuilder.NewSubPkg("expected").
+								WithFile("expected.txt", "My kptfile and I should be the only objects"))))),
+		},
+	}
+	for tn, tc := range testCases {
+
+		t.Run(tn, func(t *testing.T) {
+			expectedName := "expected"
+			repos, rw, clean := testutil.SetupReposAndWorkspace(t, map[string][]testutil.Content{
+				testutil.Upstream: {
+					{
+						Pkg:    tc.upstreamPkg,
+						Branch: "main",
+						Tag:    tc.tag,
+					},
+				},
+			})
+
+			defer clean()
+
+			g := repos[testutil.Upstream]
+			err := createKptfile(rw, &kptfilev1alpha2.Git{
+				Repo:      g.RepoDirectory,
+				Directory: tc.dir,
+				Ref:       tc.tag,
+			}, kptfilev1alpha2.ResourceMerge)
+			if !assert.NoError(t, err) {
+				t.FailNow()
+			}
+
+			err = setKptfileName(rw, expectedName)
+			if !assert.NoError(t, err) {
+				t.FailNow()
+			}
+
+			actualPkg := pkgtesting.CreatePkgOrFail(t, rw.FullPackagePath())
+			err = Command{
+				Pkg: actualPkg,
+			}.Run(fake.CtxWithNilPrinter())
+			if !assert.NoError(t, err) {
+				t.FailNow()
+			}
+
+			if !g.AssertEqual(t, rw.WorkspaceDirectory, actualPkg.UniquePath.String(), false) {
+				t.FailNow()
+			}
+			expectedPkg := pkgbuilder.NewRootPkg().
+				WithKptfile(
+					pkgbuilder.NewKptfile().
+						WithUpstreamRef(testutil.Upstream, tc.dir, tc.tag, "resource-merge").
+						WithUpstreamLockRef(testutil.Upstream, tc.dir, tc.tag, 0),
+				).WithFile("expected.txt", "My kptfile and I should be the only objects")
+			expectedPath := expectedPkg.ExpandPkgWithName(t, expectedName, testutil.ToReposInfo(repos))
+			testutil.KptfileAwarePkgEqual(t, actualPkg.UniquePath.String(), expectedPath, false)
+		})
+	}
+}
+
+func TestCommand_Run_no_subdir_at_valid_tag(t *testing.T) {
+	dir := "/java/expected"
+	tag := "java/v2"
+	expectedName := "expected_dir_is_not_here"
+	repos, rw, clean := testutil.SetupReposAndWorkspace(t, map[string][]testutil.Content{
+		testutil.Upstream: {
+			{
+				Pkg: pkgbuilder.NewRootPkg().
+					WithSubPackages(pkgbuilder.NewSubPkg("java").
+						WithResource("deployment").
+						WithSubPackages(pkgbuilder.NewSubPkg("not_expected").
+							WithFile("expected.txt", "My kptfile and I should be the only objects"))),
+				Branch: "main",
+				Tag:    tag,
+			},
+		},
+	})
+
+	defer clean()
+
+	g := repos[testutil.Upstream]
+	err := createKptfile(rw, &kptfilev1alpha2.Git{
+		Repo:      g.RepoDirectory,
+		Directory: dir,
+		Ref:       tag,
+	}, kptfilev1alpha2.ResourceMerge)
+	if !assert.NoError(t, err) {
+		t.FailNow()
+	}
+
+	err = setKptfileName(rw, expectedName)
+	if !assert.NoError(t, err) {
+		t.FailNow()
+	}
+
+	actualPkg := pkgtesting.CreatePkgOrFail(t, rw.FullPackagePath())
+	err = Command{
+		Pkg: actualPkg,
+	}.Run(fake.CtxWithNilPrinter())
+	if !assert.Error(t, err) {
+		t.FailNow()
+	}
+	assert.Contains(t, err.Error(), "no such file or directory")
+}
+
+func TestCommand_Run_no_subdir_at_invalid_tag(t *testing.T) {
+	dir := "/java/expected"
+	nonexistentTag := "notjava/v2"
+	expectedName := "expected_dir_is_here"
+	repos, rw, clean := testutil.SetupReposAndWorkspace(t, map[string][]testutil.Content{
+		testutil.Upstream: {
+			{
+				Pkg: pkgbuilder.NewRootPkg().
+					WithSubPackages(pkgbuilder.NewSubPkg("java").
+						WithResource("deployment").
+						WithSubPackages(pkgbuilder.NewSubPkg(expectedName).
+							WithFile("expected.txt", "My kptfile and I should be the only objects"))),
+				Branch: "main",
+				Tag:    "java/v2",
+			},
+		},
+	})
+
+	defer clean()
+
+	g := repos[testutil.Upstream]
+	err := createKptfile(rw, &kptfilev1alpha2.Git{
+		Repo:      g.RepoDirectory,
+		Directory: dir,
+		Ref:       nonexistentTag,
+	}, kptfilev1alpha2.ResourceMerge)
+	if !assert.NoError(t, err) {
+		t.FailNow()
+	}
+
+	err = setKptfileName(rw, expectedName)
+	if !assert.NoError(t, err) {
+		t.FailNow()
+	}
+
+	actualPkg := pkgtesting.CreatePkgOrFail(t, rw.FullPackagePath())
+	err = Command{
+		Pkg: actualPkg,
+	}.Run(fake.CtxWithNilPrinter())
+	if !assert.Error(t, err) {
+		t.FailNow()
+	}
+	assert.Contains(t, err.Error(), "unknown revision")
+}
+
 func TestCommand_Run_failInvalidRepo(t *testing.T) {
 	_, w, clean := setupWorkspace(t)
 	defer clean()
@@ -447,7 +632,7 @@ func TestCommand_Run_failInvalidRepo(t *testing.T) {
 	}
 
 	err = Command{
-		Pkg: createPackage(t, w.FullPackagePath()),
+		Pkg: pkgtesting.CreatePkgOrFail(t, w.FullPackagePath()),
 	}.Run(fake.CtxWithNilPrinter())
 	if !assert.Error(t, err) {
 		t.FailNow()
@@ -471,7 +656,7 @@ func TestCommand_Run_failInvalidBranch(t *testing.T) {
 	}
 
 	err = Command{
-		Pkg: createPackage(t, w.FullPackagePath()),
+		Pkg: pkgtesting.CreatePkgOrFail(t, w.FullPackagePath()),
 	}.Run(fake.CtxWithNilPrinter())
 	if !assert.Error(t, err) {
 		t.FailNow()
@@ -498,7 +683,7 @@ func TestCommand_Run_failInvalidTag(t *testing.T) {
 	}
 
 	err = Command{
-		Pkg: createPackage(t, w.FullPackagePath()),
+		Pkg: pkgtesting.CreatePkgOrFail(t, w.FullPackagePath()),
 	}.Run(fake.CtxWithNilPrinter())
 	if !assert.Error(t, err) {
 		t.FailNow()
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/pkg/test/runner/runner.go b/pkg/test/runner/runner.go
index 998ccf1c..7883db57 100644
--- a/pkg/test/runner/runner.go
+++ b/pkg/test/runner/runner.go
@@ -331,16 +331,16 @@ func (r *Runner) runFnRender() error {
 		if err != nil {
 			return err
 		}
-		// we passed result check, now we should break if the command error
-		// is expected
-		if fnErr != nil {
-			break
-		}
-
+		// we passed result check, now we should run teardown script and break
+		// if the command error is expected
 		err = r.runTearDownScript(pkgPath)
 		if err != nil {
 			return err
 		}
+		if fnErr != nil {
+			break
+		}
+
 	}
 	return 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",
diff --git a/thirdparty/cli-utils/status/cmdstatus_test.go b/thirdparty/cli-utils/status/cmdstatus_test.go
index 76462f44..0863e6f1 100644
--- a/thirdparty/cli-utils/status/cmdstatus_test.go
+++ b/thirdparty/cli-utils/status/cmdstatus_test.go
@@ -4,36 +4,30 @@
 package status
 
 import (
-	"bytes"
 	"context"
+	"path/filepath"
 	"strings"
 	"testing"
 	"time"
 
-	"github.com/spf13/cobra"
+	"github.com/GoogleContainerTools/kpt/internal/printer/fake"
+	"github.com/GoogleContainerTools/kpt/internal/testutil"
+	kptfilev1alpha2 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1alpha2"
+	"github.com/GoogleContainerTools/kpt/pkg/kptfile/kptfileutil"
+	"github.com/GoogleContainerTools/kpt/pkg/live"
 	"github.com/stretchr/testify/assert"
 	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/cli-runtime/pkg/genericclioptions"
 	cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
 	cmdutil "k8s.io/kubectl/pkg/cmd/util"
 	"sigs.k8s.io/cli-utils/pkg/apply/poller"
 	"sigs.k8s.io/cli-utils/pkg/kstatus/polling"
 	pollevent "sigs.k8s.io/cli-utils/pkg/kstatus/polling/event"
 	"sigs.k8s.io/cli-utils/pkg/kstatus/status"
-	"sigs.k8s.io/cli-utils/pkg/manifestreader"
 	"sigs.k8s.io/cli-utils/pkg/object"
-	"sigs.k8s.io/cli-utils/pkg/provider"
 )
 
 var (
-	inventoryTemplate = `
-kind: ConfigMap
-apiVersion: v1
-metadata:
-  labels:
-    cli-utils.sigs.k8s.io/inventory-id: test
-  name: foo
-  namespace: default
-`
 	depObject = object.ObjMetadata{
 		Name:      "foo",
 		Namespace: "default",
@@ -58,24 +52,36 @@ func TestStatusCommand(t *testing.T) {
 		pollUntil      string
 		printer        string
 		timeout        time.Duration
-		input          string
+		kptfileInv     *kptfilev1alpha2.Inventory
 		inventory      []object.ObjMetadata
 		events         []pollevent.Event
 		expectedErrMsg string
 		expectedOutput string
 	}{
 		"no inventory template": {
-			input:          "",
-			expectedErrMsg: "Package uninitialized. Please run \"init\" command.",
+			kptfileInv:     nil,
+			expectedErrMsg: "inventory info not found in Kptfile",
+		},
+		"invalid value for pollUntil": {
+			pollUntil:      "doesNotExist",
+			expectedErrMsg: "pollUntil must be one of \"known\", \"current\", \"deleted\", \"forever\"",
 		},
 		"no inventory in live state": {
-			input:          inventoryTemplate,
+			kptfileInv: &kptfilev1alpha2.Inventory{
+				Name:        "foo",
+				Namespace:   "default",
+				InventoryID: "test",
+			},
 			expectedOutput: "no resources found in the inventory\n",
 		},
 		"wait for all known": {
 			pollUntil: "known",
 			printer:   "events",
-			input:     inventoryTemplate,
+			kptfileInv: &kptfilev1alpha2.Inventory{
+				Name:        "foo",
+				Namespace:   "default",
+				InventoryID: "test",
+			},
 			inventory: []object.ObjMetadata{
 				depObject,
 				stsObject,
@@ -106,7 +112,11 @@ statefulset.apps/bar is Current: current
 		"wait for all current": {
 			pollUntil: "current",
 			printer:   "events",
-			input:     inventoryTemplate,
+			kptfileInv: &kptfilev1alpha2.Inventory{
+				Name:        "foo",
+				Namespace:   "default",
+				InventoryID: "test",
+			},
 			inventory: []object.ObjMetadata{
 				depObject,
 				stsObject,
@@ -155,7 +165,11 @@ deployment.apps/foo is Current: current
 		"wait for all deleted": {
 			pollUntil: "deleted",
 			printer:   "events",
-			input:     inventoryTemplate,
+			kptfileInv: &kptfilev1alpha2.Inventory{
+				Name:        "foo",
+				Namespace:   "default",
+				InventoryID: "test",
+			},
 			inventory: []object.ObjMetadata{
 				depObject,
 				stsObject,
@@ -187,7 +201,11 @@ deployment.apps/foo is NotFound: notFound
 			pollUntil: "forever",
 			printer:   "events",
 			timeout:   2 * time.Second,
-			input:     inventoryTemplate,
+			kptfileInv: &kptfilev1alpha2.Inventory{
+				Name:        "foo",
+				Namespace:   "default",
+				InventoryID: "test",
+			},
 			inventory: []object.ObjMetadata{
 				depObject,
 				stsObject,
@@ -221,27 +239,37 @@ deployment.apps/foo is InProgress: inProgress
 		t.Run(tn, func(t *testing.T) {
 			tf := cmdtesting.NewTestFactory().WithNamespace("namespace")
 			defer tf.Cleanup()
+			ioStreams, _, outBuf, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled
 
-			provider := provider.NewFakeProvider(tf, tc.inventory)
-			loader := manifestreader.NewFakeLoader(tf, tc.inventory)
-			runner := &StatusRunner{
-				provider: provider,
-				loader:   loader,
-				pollerFactoryFunc: func(c cmdutil.Factory) (poller.Poller, error) {
-					return &fakePoller{tc.events}, nil
-				},
+			w, clean := testutil.SetupWorkspace(t)
+			defer clean()
+			kf := kptfileutil.DefaultKptfile(filepath.Base(w.WorkspaceDirectory))
+			kf.Inventory = tc.kptfileInv
+			testutil.AddKptfileToWorkspace(t, w, kf)
 
-				pollUntil: tc.pollUntil,
-				output:    tc.printer,
-				timeout:   tc.timeout,
-			}
+			revert := testutil.Chdir(t, w.WorkspaceDirectory)
+			defer revert()
 
-			cmd := &cobra.Command{}
-			cmd.SetIn(strings.NewReader(tc.input))
-			var buf bytes.Buffer
-			cmd.SetOut(&buf)
+			provider := live.NewFakeResourceGroupProvider(tf, tc.inventory)
+			loader := live.NewResourceGroupManifestLoader(tf)
+			runner := NewRunner(fake.CtxWithNilPrinter(), provider, loader, ioStreams)
+			runner.pollerFactoryFunc = func(c cmdutil.Factory) (poller.Poller, error) {
+				return &fakePoller{tc.events}, nil
+			}
 
-			err := runner.runE(cmd, []string{})
+			args := []string{}
+			if tc.pollUntil != "" {
+				args = append(args, []string{
+					"--poll-until", tc.pollUntil,
+				}...)
+			}
+			if tc.timeout != time.Duration(0) {
+				args = append(args, []string{
+					"--timeout", tc.timeout.String(),
+				}...)
+			}
+			runner.Command.SetArgs(args)
+			err := runner.Command.Execute()
 
 			if tc.expectedErrMsg != "" {
 				if !assert.Error(t, err) {
@@ -252,7 +280,7 @@ deployment.apps/foo is InProgress: inProgress
 			}
 
 			assert.NoError(t, err)
-			assert.Equal(t, strings.TrimSpace(buf.String()), strings.TrimSpace(tc.expectedOutput))
+			assert.Equal(t, strings.TrimSpace(tc.expectedOutput), strings.TrimSpace(outBuf.String()))
 		})
 	}
 }