Repository: GoogleContainerTools/kpt
Pull request #2032 (2021-05-17 21:02:28)

Support bash script in the e2e test runner
If the script is given, the runner will run the script instead of running kpt commands.


Patch:
diff --git a/Makefile b/Makefile
index 5ea83c49..5e11bd51 100644
--- a/Makefile
+++ b/Makefile
@@ -65,16 +65,16 @@ test:
 # This target is used to run Go tests that require docker runtime.
 # Some tests, like pipeline tests, need to have docker available to run.
 test-docker: build
-	KPT_E2E_BIN=$(GOBIN)/kpt go test -cover --tags=docker ./...
+	PATH=$(GOBIN):$(PATH) go test -cover --tags=docker ./...
 
 # 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
-	KPT_E2E_BIN=$(GOBIN)/kpt go test -v --tags=docker --run=TestFnRender ./e2e/
+	PATH=$(GOBIN):$(PATH) go test -v --tags=docker --run=TestFnRender ./e2e/
 
 # target to run e2e tests for "kpt fn eval" command
 test-fn-eval: build
-	KPT_E2E_BIN=$(GOBIN)/kpt go test -v --tags=docker --run=TestFnEval ./e2e/
+	PATH=$(GOBIN):$(PATH) go test -v --tags=docker --run=TestFnEval ./e2e/
 
 # target to flush kpt-fn cache
 flush-fn-cache:
diff --git a/commands/livecmd.go b/commands/livecmd.go
index 86e9cce5..5123bd9c 100644
--- a/commands/livecmd.go
+++ b/commands/livecmd.go
@@ -21,11 +21,12 @@ import (
 	"os"
 
 	"github.com/GoogleContainerTools/kpt/internal/cmdapply"
+	"github.com/GoogleContainerTools/kpt/internal/cmddestroy"
 	"github.com/GoogleContainerTools/kpt/internal/cmdliveinit"
+	"github.com/GoogleContainerTools/kpt/internal/cmdmigrate"
 	"github.com/GoogleContainerTools/kpt/internal/docs/generated/livedocs"
 	"github.com/GoogleContainerTools/kpt/internal/util/cfgflags"
 	"github.com/GoogleContainerTools/kpt/pkg/live"
-	"github.com/GoogleContainerTools/kpt/thirdparty/cli-utils/destroy"
 	"github.com/GoogleContainerTools/kpt/thirdparty/cli-utils/diff"
 	"github.com/GoogleContainerTools/kpt/thirdparty/cli-utils/preview"
 	"github.com/GoogleContainerTools/kpt/thirdparty/cli-utils/status"
@@ -69,17 +70,17 @@ func GetLiveCommand(ctx context.Context, _, version string) *cobra.Command {
 	dp := live.NewDualDelegatingProvider(f)
 	dl := live.NewDualDelegatingManifestReader(f)
 
-	rgp := live.NewResourceGroupProvider(f)
-	rgl := live.NewResourceGroupManifestLoader(f)
+	rgProvider := live.NewResourceGroupProvider(f)
+	rgLoader := live.NewResourceGroupManifestLoader(f)
+
+	cmProvider := provider.NewProvider(f)
+	cmLoader := manifestreader.NewManifestLoader(f)
 
 	// Init command which updates a Kptfile for the ResourceGroup inventory object.
 	klog.V(2).Infoln("init command updates Kptfile for ResourceGroup inventory")
-	initCmd := cmdliveinit.NewCmdInit(f, ioStreams)
-	initCmd.Short = livedocs.InitShort
-	initCmd.Long = livedocs.InitShort + "\n" + livedocs.InitLong
-	initCmd.Example = livedocs.InitExamples
+	initCmd := cmdliveinit.NewCommand(ctx, f, ioStreams)
 
-	applyCmd := cmdapply.NewCommand(ctx, rgp, rgl, ioStreams)
+	applyCmd := cmdapply.NewCommand(ctx, rgProvider, rgLoader, ioStreams)
 
 	previewCmd := preview.GetPreviewRunner(dp, dl, ioStreams).Command
 	previewCmd.Short = livedocs.PreviewShort
@@ -91,10 +92,7 @@ func GetLiveCommand(ctx context.Context, _, version string) *cobra.Command {
 	diffCmd.Long = livedocs.DiffShort + "\n" + livedocs.DiffLong
 	diffCmd.Example = livedocs.DiffExamples
 
-	destroyCmd := destroy.GetDestroyRunner(dp, dl, ioStreams).Command
-	destroyCmd.Short = livedocs.DestroyShort
-	destroyCmd.Long = livedocs.DestroyShort + "\n" + livedocs.DestroyLong
-	destroyCmd.Example = livedocs.DestroyExamples
+	destroyCmd := cmddestroy.NewCommand(ctx, rgProvider, rgLoader, ioStreams)
 
 	statusCmd := status.GetStatusRunner(dp, dl).Command
 	statusCmd.Short = livedocs.StatusShort
@@ -107,11 +105,7 @@ func GetLiveCommand(ctx context.Context, _, version string) *cobra.Command {
 	// Add the migrate command to change from ConfigMap to ResourceGroup inventory
 	// object. Also add the install-resource-group command.
 	klog.V(2).Infoln("adding kpt live migrate command")
-	cmProvider := provider.NewProvider(f)
-	rgProvider := live.NewResourceGroupProvider(f)
-	cmLoader := manifestreader.NewManifestLoader(f)
-	rgLoader := live.NewResourceGroupManifestLoader(f)
-	migrateCmd := GetMigrateRunner(cmProvider, rgProvider, cmLoader, rgLoader, ioStreams).Command
+	migrateCmd := cmdmigrate.NewCommand(ctx, cmProvider, rgProvider, cmLoader, rgLoader, ioStreams)
 	installRGCmd := GetInstallRGRunner(f, ioStreams).Command
 	liveCmd.AddCommand(migrateCmd, installRGCmd)
 
diff --git a/internal/cmddestroy/cmddestroy.go b/internal/cmddestroy/cmddestroy.go
new file mode 100644
index 00000000..579b6c5a
--- /dev/null
+++ b/internal/cmddestroy/cmddestroy.go
@@ -0,0 +1,160 @@
+// 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 cmddestroy
+
+import (
+	"context"
+	"fmt"
+	"os"
+
+	"github.com/GoogleContainerTools/kpt/internal/cmdutil"
+	"github.com/GoogleContainerTools/kpt/internal/docs/generated/livedocs"
+	"github.com/GoogleContainerTools/kpt/thirdparty/cli-utils/flagutils"
+	"github.com/GoogleContainerTools/kpt/thirdparty/cli-utils/printers"
+	"github.com/spf13/cobra"
+	"k8s.io/cli-runtime/pkg/genericclioptions"
+	"sigs.k8s.io/cli-utils/pkg/apply"
+	"sigs.k8s.io/cli-utils/pkg/common"
+	"sigs.k8s.io/cli-utils/pkg/inventory"
+	"sigs.k8s.io/cli-utils/pkg/manifestreader"
+	"sigs.k8s.io/cli-utils/pkg/provider"
+)
+
+func NewRunner(ctx context.Context, provider provider.Provider, loader manifestreader.ManifestLoader,
+	ioStreams genericclioptions.IOStreams) *Runner {
+
+	r := &Runner{
+		ctx:           ctx,
+		Destroyer:     apply.NewDestroyer(provider),
+		provider:      provider,
+		loader:        loader,
+		ioStreams:     ioStreams,
+		destroyRunner: runDestroy,
+	}
+	c := &cobra.Command{
+		Use:     "destroy [PKG_PATH | -]",
+		RunE:    r.runE,
+		PreRunE: r.preRunE,
+		Short:   livedocs.DestroyShort,
+		Long:    livedocs.DestroyShort + "\n" + livedocs.DestroyLong,
+		Example: livedocs.DestroyExamples,
+	}
+	r.Command = c
+
+	c.Flags().StringVar(&r.output, "output", printers.DefaultPrinter(),
+		fmt.Sprintf("Output format, must be one of %s", cmdutil.JoinStringsWithQuotes(printers.SupportedPrinters())))
+	c.Flags().StringVar(&r.inventoryPolicyString, flagutils.InventoryPolicyFlag, flagutils.InventoryPolicyStrict,
+		"It determines the behavior when the resources don't belong to current inventory. Available options "+
+			fmt.Sprintf("%q and %q.", flagutils.InventoryPolicyStrict, flagutils.InventoryPolicyAdopt))
+	return r
+}
+
+// NewCommand returns a cobra command.
+func NewCommand(ctx context.Context, provider provider.Provider, loader manifestreader.ManifestLoader,
+	ioStreams genericclioptions.IOStreams) *cobra.Command {
+	return NewRunner(ctx, provider, loader, ioStreams).Command
+}
+
+// Runner contains the run function that contains the cli functionality for the
+// destroy command.
+type Runner struct {
+	ctx        context.Context
+	Command    *cobra.Command
+	PreProcess func(info inventory.InventoryInfo, strategy common.DryRunStrategy) (inventory.InventoryPolicy, error)
+	Destroyer  *apply.Destroyer
+	provider   provider.Provider
+	loader     manifestreader.ManifestLoader
+	ioStreams  genericclioptions.IOStreams
+
+	output                string
+	inventoryPolicyString string
+
+	inventoryPolicy inventory.InventoryPolicy
+
+	// TODO(mortent): This is needed for now since we don't have a good way to
+	// stub out the Destroyer with an interface for testing purposes.
+	destroyRunner func(r *Runner, inv inventory.InventoryInfo) error
+}
+
+// preRunE validates the inventoryPolicy and the output type.
+func (r *Runner) preRunE(_ *cobra.Command, _ []string) error {
+	var err error
+	r.inventoryPolicy, err = flagutils.ConvertInventoryPolicy(r.inventoryPolicyString)
+	if err != nil {
+		return err
+	}
+
+	if found := printers.ValidatePrinterType(r.output); !found {
+		return fmt.Errorf("unknown output type %q", r.output)
+	}
+
+	return nil
+}
+
+// runE handles the input flags and args, sets up the Destroyer, and
+// invokes the
+func (r *Runner) runE(c *cobra.Command, args []string) error {
+	if len(args) == 0 {
+		// default to the current working directory
+		cwd, err := os.Getwd()
+		if err != nil {
+			return err
+		}
+		args = append(args, cwd)
+	}
+
+	reader, err := r.loader.ManifestReader(c.InOrStdin(), args[0])
+	if err != nil {
+		return err
+	}
+
+	objs, err := reader.Read()
+	if err != nil {
+		return err
+	}
+
+	inv, _, err := r.loader.InventoryInfo(objs)
+	if err != nil {
+		return err
+	}
+
+	// TODO(mortent): Figure out if we can do this differently.
+	if r.PreProcess != nil {
+		r.inventoryPolicy, err = r.PreProcess(inv, r.Destroyer.DryRunStrategy)
+		if err != nil {
+			return err
+		}
+	}
+
+	return r.destroyRunner(r, inv)
+}
+
+func runDestroy(r *Runner, inv inventory.InventoryInfo) error {
+	// Run the destroyer. It will return a channel where we can receive updates
+	// to keep track of progress and any issues.
+	err := r.Destroyer.Initialize()
+	if err != nil {
+		return err
+	}
+	option := &apply.DestroyerOption{
+		InventoryPolicy: r.inventoryPolicy,
+	}
+	ch := r.Destroyer.Run(inv, option)
+
+	// The printer will print updates from the channel. It will block
+	// until the channel is closed.
+	printer := printers.GetPrinter(r.output, r.ioStreams)
+	return printer.Print(ch, r.Destroyer.DryRunStrategy)
+}
diff --git a/internal/cmdliveinit/cmdliveinit.go b/internal/cmdliveinit/cmdliveinit.go
index e6a5558d..5f81e04f 100644
--- a/internal/cmdliveinit/cmdliveinit.go
+++ b/internal/cmdliveinit/cmdliveinit.go
@@ -4,12 +4,15 @@
 package cmdliveinit
 
 import (
+	"context"
 	"crypto/sha1"
 	"fmt"
+	"os"
 	"strconv"
 	"strings"
 	"time"
 
+	"github.com/GoogleContainerTools/kpt/internal/printer"
 	kptfilev1alpha2 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1alpha2"
 	"github.com/GoogleContainerTools/kpt/pkg/kptfile/kptfileutil"
 	"github.com/spf13/cobra"
@@ -27,17 +30,41 @@ const defaultInventoryName = "inventory"
 type InvExistsError struct{}
 
 func (i *InvExistsError) Error() string {
-	return InvExistsErrorMsg
+	return "inventory information already set for package"
 }
 
-var InvExistsErrorMsg = `ResourceGroup configuration has already been created. Changing
-them after a package has been applied to the cluster can lead to
-undesired results. Use the --force flag to suppress this error.
-`
+func NewRunner(ctx context.Context, factory cmdutil.Factory,
+	ioStreams genericclioptions.IOStreams) *Runner {
+	r := &Runner{
+		ctx:       ctx,
+		factory:   factory,
+		ioStreams: ioStreams,
+	}
+
+	cmd := &cobra.Command{
+		Use:                   "init [PKG_PATH]",
+		DisableFlagsInUseLine: true,
+		Short:                 i18n.T("Initialize inventory parameters into Kptfile"),
+		RunE:                  r.runE,
+	}
+	r.Command = cmd
+
+	cmd.Flags().StringVar(&r.Name, "name", "", "Inventory object name")
+	cmd.Flags().BoolVar(&r.Force, "force", false, "Set inventory values even if already set in Kptfile")
+	cmd.Flags().BoolVar(&r.Quiet, "quiet", false, "If true, do not print output message for initialization")
+	cmd.Flags().StringVar(&r.InventoryID, "inventory-id", "", "Inventory id for the package")
+	return r
+}
+
+func NewCommand(ctx context.Context, f cmdutil.Factory,
+	ioStreams genericclioptions.IOStreams) *cobra.Command {
+	return NewRunner(ctx, f, ioStreams).Command
+}
+
+type Runner struct {
+	ctx     context.Context
+	Command *cobra.Command
 
-// KptInitOptions encapsulates fields for kpt init command. This init command
-// fills in inventory values in the Kptfile.
-type KptInitOptions struct {
 	factory     cmdutil.Factory
 	ioStreams   genericclioptions.IOStreams
 	dir         string // Directory with Kptfile
@@ -48,60 +75,117 @@ type KptInitOptions struct {
 	Quiet       bool   // Output message during initialization
 }
 
-// NewKptInitOptions returns a pointer to an initial KptInitOptions structure.
-func NewKptInitOptions(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *KptInitOptions {
-	return &KptInitOptions{
-		factory:   f,
-		ioStreams: ioStreams,
-		Quiet:     false,
+func (r *Runner) runE(_ *cobra.Command, args []string) error {
+	if len(args) == 0 {
+		// default to the current working directory
+		cwd, err := os.Getwd()
+		if err != nil {
+			return err
+		}
+		args = append(args, cwd)
 	}
-}
 
-// Complete fills in fields for KptInitOptions based on the passed "args".
-func (io *KptInitOptions) Run(args []string) error {
-	// Set the init options directory.
-	if len(args) != 1 {
-		return fmt.Errorf("need one 'directory' arg; have %d", len(args))
-	}
 	dir, err := config.NormalizeDir(args[0])
 	if err != nil {
 		return err
 	}
-	io.dir = dir
-	// Set the init options inventory object namespace.
-	ns, err := config.FindNamespace(io.factory.ToRawKubeConfigLoader(), io.dir)
+	r.dir = dir
+
+	return (&ConfigureInventoryInfo{
+		Path:        dir,
+		Factory:     r.factory,
+		Quiet:       r.Quiet,
+		Name:        r.Name,
+		InventoryID: r.InventoryID,
+		Force:       r.Force,
+	}).Run(r.ctx)
+}
+
+// ConfigureInventoryInfo contains the functionality for adding and updating
+// the inventory information in the Kptfile.
+type ConfigureInventoryInfo struct {
+	Path    string
+	Factory cmdutil.Factory
+	Quiet   bool
+
+	Name        string
+	InventoryID string
+
+	Force bool
+}
+
+// Run updates the inventory info in the package given by the Path.
+func (c *ConfigureInventoryInfo) Run(ctx context.Context) error {
+	pr := printer.FromContextOrDie(ctx)
+
+	var name, namespace, inventoryID string
+
+	ns, err := config.FindNamespace(c.Factory.ToRawKubeConfigLoader(), c.Path)
 	if err != nil {
 		return err
 	}
-	io.namespace = strings.TrimSpace(ns)
-	if !io.Quiet {
-		fmt.Fprintf(io.ioStreams.Out, "initializing Kptfile inventory info (namespace: %s)...", io.namespace)
+	namespace = strings.TrimSpace(ns)
+	if !c.Quiet {
+		pr.Printf("initializing Kptfile inventory info (namespace: %s)...", namespace)
 	}
-	// Set the init options default inventory object name, if not set by flag.
-	if io.Name == "" {
+
+	// Autogenerate the name if it is not provided through the flag.
+	if c.Name == "" {
 		randomSuffix := common.RandomStr(time.Now().UTC().UnixNano())
-		io.Name = fmt.Sprintf("%s-%s", defaultInventoryName, randomSuffix)
+		name = fmt.Sprintf("%s-%s", defaultInventoryName, randomSuffix)
+	} else {
+		name = c.Name
 	}
-	// Set the init options inventory id label, if not already set.
-	if io.InventoryID == "" {
-		id, err := generateID(io.Name, io.namespace, time.Now())
+	// Generate the inventory id if one is not specified through a flag.
+	if c.InventoryID == "" {
+		id, err := generateID(namespace, name, time.Now())
 		if err != nil {
 			return err
 		}
-		io.InventoryID = id
+		inventoryID = id
+	} else {
+		inventoryID = c.InventoryID
 	}
 	// Finally, update these values in the Inventory section of the Kptfile.
-	err = io.updateKptfile()
-	if !io.Quiet {
+	err = updateKptfile(c.Path, &kptfilev1alpha2.Inventory{
+		Namespace:   namespace,
+		Name:        name,
+		InventoryID: inventoryID,
+	}, c.Force)
+	if !c.Quiet {
 		if err == nil {
-			fmt.Fprintln(io.ioStreams.Out, "success")
+			pr.Printf("success")
 		} else {
-			fmt.Fprintln(io.ioStreams.Out, "failed")
+			pr.Printf("failed")
 		}
 	}
 	return err
 }
 
+// Run fills in the inventory object values into the Kptfile.
+func updateKptfile(path string, inv *kptfilev1alpha2.Inventory, force bool) error {
+	// Read the Kptfile io io.dir
+	kf, err := kptfileutil.ReadFile(path)
+	if err != nil {
+		return err
+	}
+	// Validate the inventory values don't already exist
+	isEmpty := kptfileInventoryEmpty(kf.Inventory)
+	if !isEmpty && !force {
+		return &InvExistsError{}
+	}
+	// Check the new inventory values are valid.
+	if err := inv.Validate(); err != nil {
+		return 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
+	}
+	return nil
+}
+
 // generateID returns the string which is a SHA1 hash of the passed namespace
 // and name, with the unix timestamp string concatenated. Returns an error
 // if either the namespace or name are empty.
@@ -128,74 +212,8 @@ func generateHash(namespace string, name string) (string, error) {
 	return fmt.Sprintf("%x", (h.Sum(nil))), nil
 }
 
-// Run fills in the inventory object values into the Kptfile.
-func (io *KptInitOptions) updateKptfile() error {
-	// Read the Kptfile io io.dir
-	kf, err := kptfileutil.ReadFile(io.dir)
-	if err != nil {
-		return err
-	}
-	// Validate the inventory values don't already exist
-	isEmpty := kptfileInventoryEmpty(kf.Inventory)
-	if !isEmpty && !io.Force {
-		return &InvExistsError{}
-	}
-	// Check the new inventory values are valid.
-	if err := io.validate(); err != nil {
-		return err
-	}
-	// Finally, set the inventory parameters in the Kptfile and write it.
-	kf.Inventory = &kptfilev1alpha2.Inventory{
-		Namespace:   io.namespace,
-		Name:        io.Name,
-		InventoryID: io.InventoryID,
-	}
-	if err := kptfileutil.WriteFile(io.dir, kf); err != nil {
-		return err
-	}
-	return nil
-}
-
-// validate ensures the inventory object parameters are valid.
-func (io *KptInitOptions) validate() error {
-	// name is required
-	if len(io.Name) == 0 {
-		return fmt.Errorf("inventory name is missing")
-	}
-	// namespace is required
-	if len(io.namespace) == 0 {
-		return fmt.Errorf("inventory namespace is missing")
-	}
-	// inventoryID is required
-	if len(io.InventoryID) == 0 {
-		return fmt.Errorf("inventoryID is missing")
-	}
-	return nil
-}
-
 // kptfileInventoryEmpty returns true if the Inventory structure
 // in the Kptfile is empty; false otherwise.
 func kptfileInventoryEmpty(inv *kptfilev1alpha2.Inventory) bool {
 	return inv == nil
 }
-
-// NewCmdInit returns the cobra command for the init command.
-func NewCmdInit(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
-	io := NewKptInitOptions(f, ioStreams)
-	cmd := &cobra.Command{
-		Use:                   "init [PKG_PATH]",
-		DisableFlagsInUseLine: true,
-		Short:                 i18n.T("Initialize inventory parameters into Kptfile"),
-		RunE: func(cmd *cobra.Command, args []string) error {
-			if len(args) == 0 {
-				// default to current working directory
-				args = append(args, ".")
-			}
-			return io.Run(args)
-		},
-	}
-	cmd.Flags().StringVar(&io.Name, "name", "", "Inventory object name")
-	cmd.Flags().BoolVar(&io.Force, "force", false, "Set inventory values even if already set in Kptfile")
-	cmd.Flags().BoolVar(&io.Quiet, "quiet", false, "If true, do not print output message for initialization")
-	return cmd
-}
diff --git a/commands/migratecmd.go b/internal/cmdmigrate/migratecmd.go
similarity index 84%
rename from commands/migratecmd.go
rename to internal/cmdmigrate/migratecmd.go
index 5848af0c..d43e88f5 100644
--- a/commands/migratecmd.go
+++ b/internal/cmdmigrate/migratecmd.go
@@ -1,10 +1,12 @@
 // Copyright 2020 Google LLC.
 // SPDX-License-Identifier: Apache-2.0
 
-package commands
+package cmdmigrate
 
 import (
 	"bytes"
+	"context"
+	"errors"
 	"fmt"
 	"io"
 	"io/ioutil"
@@ -16,7 +18,6 @@ import (
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/cli-runtime/pkg/genericclioptions"
 	"k8s.io/klog"
-	cmdutil "k8s.io/kubectl/pkg/cmd/util"
 	"k8s.io/kubectl/pkg/util/i18n"
 	"sigs.k8s.io/cli-utils/pkg/common"
 	"sigs.k8s.io/cli-utils/pkg/config"
@@ -28,31 +29,33 @@ import (
 
 // MigrateRunner encapsulates fields for the kpt migrate command.
 type MigrateRunner struct {
+	ctx       context.Context
 	Command   *cobra.Command
 	ioStreams genericclioptions.IOStreams
 
-	dir         string
-	dryRun      bool
-	initOptions *cmdliveinit.KptInitOptions
-	cmProvider  provider.Provider
-	rgProvider  provider.Provider
-	cmLoader    manifestreader.ManifestLoader
-	rgLoader    manifestreader.ManifestLoader
+	dir        string
+	dryRun     bool
+	name       string
+	force      bool
+	cmProvider provider.Provider
+	rgProvider provider.Provider
+	cmLoader   manifestreader.ManifestLoader
+	rgLoader   manifestreader.ManifestLoader
 }
 
-// NewMigrateRunner returns a pointer to an initial MigrateRunner structure.
-func GetMigrateRunner(cmProvider provider.Provider, rgProvider provider.Provider,
+// NewRunner returns a pointer to an initial MigrateRunner structure.
+func NewRunner(ctx context.Context, cmProvider provider.Provider, rgProvider provider.Provider,
 	cmLoader manifestreader.ManifestLoader, rgLoader manifestreader.ManifestLoader,
 	ioStreams genericclioptions.IOStreams) *MigrateRunner {
 	r := &MigrateRunner{
-		ioStreams:   ioStreams,
-		dryRun:      false,
-		initOptions: cmdliveinit.NewKptInitOptions(cmProvider.Factory(), ioStreams),
-		cmProvider:  cmProvider,
-		rgProvider:  rgProvider,
-		cmLoader:    cmLoader,
-		rgLoader:    rgLoader,
-		dir:         "",
+		ctx:        ctx,
+		ioStreams:  ioStreams,
+		dryRun:     false,
+		cmProvider: cmProvider,
+		rgProvider: rgProvider,
+		cmLoader:   cmLoader,
+		rgLoader:   rgLoader,
+		dir:        "",
 	}
 	cmd := &cobra.Command{
 		Use:                   "migrate [DIR | -]",
@@ -73,21 +76,19 @@ func GetMigrateRunner(cmProvider provider.Provider, rgProvider provider.Provider
 			return nil
 		},
 	}
-	cmd.Flags().StringVar(&r.initOptions.Name, "name", "", "Inventory object name")
-	cmd.Flags().BoolVar(&r.initOptions.Force, "force", false, "Set inventory values even if already set in Kptfile")
+	cmd.Flags().StringVar(&r.name, "name", "", "Inventory object name")
+	cmd.Flags().BoolVar(&r.force, "force", false, "Set inventory values even if already set in Kptfile")
 	cmd.Flags().BoolVar(&r.dryRun, "dry-run", false, "Do not actually migrate, but show steps")
 
 	r.Command = cmd
 	return r
 }
 
-// NewCmdMigrate returns the cobra command for the migrate command.
-func NewCmdMigrate(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
-	configMapProvider := provider.NewProvider(f)
-	resourceGroupProvider := live.NewResourceGroupProvider(f)
-	cmLoader := manifestreader.NewManifestLoader(f)
-	rgLoader := live.NewResourceGroupManifestLoader(f)
-	return GetMigrateRunner(configMapProvider, resourceGroupProvider, cmLoader, rgLoader, ioStreams).Command
+// NewCommand returns the cobra command for the migrate command.
+func NewCommand(ctx context.Context, cmProvider provider.Provider, rgProvider provider.Provider,
+	cmLoader manifestreader.ManifestLoader, rgLoader manifestreader.ManifestLoader,
+	ioStreams genericclioptions.IOStreams) *cobra.Command {
+	return NewRunner(ctx, cmProvider, rgProvider, cmLoader, rgLoader, ioStreams).Command
 }
 
 // Run executes the migration from the ConfigMap based inventory to the ResourceGroup
@@ -135,7 +136,7 @@ func (mr *MigrateRunner) Run(reader io.Reader, args []string) error {
 	cmInventoryID := cmInvObj.ID()
 	klog.V(4).Infof("previous inventoryID: %s", cmInventoryID)
 	// Update the Kptfile with the resource group values (e.g. namespace, name, id).
-	if err := mr.updateKptfile(args, cmInventoryID); err != nil {
+	if err := mr.updateKptfile(mr.ctx, args, cmInventoryID); err != nil {
 		return err
 	}
 	cmObjs, err := mr.retrieveInvObjs(cmInvObj)
@@ -176,14 +177,20 @@ func (mr *MigrateRunner) applyCRD() error {
 }
 
 // updateKptfile installs the "inventory" fields in the Kptfile.
-func (mr *MigrateRunner) updateKptfile(args []string, prevID string) 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 {
-		// Set inventory ID from previous inventory object.
-		mr.initOptions.InventoryID = prevID
-		mr.initOptions.Quiet = true
-		if err := mr.initOptions.Run(args); err != nil {
-			if _, exists := err.(*cmdliveinit.InvExistsError); exists {
+		err := (&cmdliveinit.ConfigureInventoryInfo{
+			Path:        args[0],
+			Factory:     mr.rgProvider.Factory(),
+			Quiet:       true,
+			InventoryID: prevID,
+			Force:       mr.force,
+		}).Run(ctx)
+
+		if err != nil {
+			var invExistsError *cmdliveinit.InvExistsError
+			if errors.As(err, &invExistsError) {
 				fmt.Fprint(mr.ioStreams.Out, "values already exist...")
 			} else {
 				return err
diff --git a/internal/cmdrender/cmd.go b/internal/cmdrender/cmd.go
index 4f9d3638..1220132e 100644
--- a/internal/cmdrender/cmd.go
+++ b/internal/cmdrender/cmd.go
@@ -23,6 +23,7 @@ import (
 	docs "github.com/GoogleContainerTools/kpt/internal/docs/generated/fndocs"
 	"github.com/GoogleContainerTools/kpt/internal/errors"
 	"github.com/GoogleContainerTools/kpt/internal/util/cmdutil"
+	"github.com/GoogleContainerTools/kpt/internal/util/pkgutil"
 	"github.com/spf13/cobra"
 )
 
@@ -36,6 +37,7 @@ func NewRunner(ctx context.Context, parent string) *Runner {
 		Example: docs.RenderExamples,
 		RunE:    r.runE,
 		PreRunE: r.preRunE,
+		PostRun: r.postRun,
 	}
 	c.Flags().StringVar(&r.resultsDirPath, "results-dir", "",
 		"path to a directory to save function results")
@@ -88,8 +90,9 @@ func (r *Runner) runE(c *cobra.Command, _ []string) error {
 		PkgPath:        r.pkgPath,
 		ResultsDirPath: r.resultsDirPath,
 	}
-	if err = executor.Execute(r.ctx); err != nil {
-		return err
-	}
-	return nil
+	return executor.Execute(r.ctx)
+}
+
+func (r *Runner) postRun(_ *cobra.Command, _ []string) {
+	pkgutil.FormatPackage(r.pkgPath)
 }
diff --git a/internal/cmdrender/executor.go b/internal/cmdrender/executor.go
index 474dd644..25c2240d 100644
--- a/internal/cmdrender/executor.go
+++ b/internal/cmdrender/executor.go
@@ -445,9 +445,6 @@ func fnChain(ctx context.Context, hctx *hydrationContext, pkgPath types.UniquePa
 
 // trackInputFiles records file paths of input resources in the hydration context.
 func trackInputFiles(hctx *hydrationContext, input []*yaml.RNode) error {
-	if err := detectPathConflicts(input); err != nil {
-		return err
-	}
 	if hctx.inputFiles == nil {
 		hctx.inputFiles = sets.String{}
 	}
@@ -464,9 +461,6 @@ func trackInputFiles(hctx *hydrationContext, input []*yaml.RNode) error {
 // trackOutputfiles records the file paths of output resources in the hydration
 // context. It should be invoked post hydration.
 func trackOutputFiles(hctx *hydrationContext) error {
-	if err := detectPathConflicts(hctx.root.resources); err != nil {
-		return err
-	}
 	outputSet := sets.String{}
 
 	for _, r := range hctx.root.resources {
@@ -480,28 +474,6 @@ func trackOutputFiles(hctx *hydrationContext) error {
 	return nil
 }
 
-// detectPathConflicts returns an error if the same index/path is on multiple resources
-func detectPathConflicts(nodes []*yaml.RNode) error {
-	// map has structure path -> index -> bool
-	// to keep track of paths and indexes found
-	pathIndexes := make(map[string]map[string]bool)
-	for _, node := range nodes {
-		fp, index, err := kioutil.GetFileAnnotations(node)
-		if err != nil {
-			return err
-		}
-		fp = path.Clean(fp)
-		if pathIndexes[fp] == nil {
-			pathIndexes[fp] = make(map[string]bool)
-		}
-		if _, ok := pathIndexes[fp][index]; ok {
-			return fmt.Errorf("resource at path %q and index %q already exists", fp, index)
-		}
-		pathIndexes[fp][index] = true
-	}
-	return nil
-}
-
 // pruneResources compares the input and output of the hydration and prunes
 // resources that are no longer present in the output of the hydration.
 func pruneResources(hctx *hydrationContext) error {
diff --git a/internal/docs/generated/fndocs/docs.go b/internal/docs/generated/fndocs/docs.go
index 512894df..2de160b9 100644
--- a/internal/docs/generated/fndocs/docs.go
+++ b/internal/docs/generated/fndocs/docs.go
@@ -32,7 +32,16 @@ Args:
   DIR|-:
     Path to the local directory containing resources. Defaults to the current
     working directory. Using '-' as the directory path will cause ` + "`" + `eval` + "`" + ` to
-    read resources from ` + "`" + `stdin` + "`" + ` and write the output to ` + "`" + `stdout` + "`" + `.
+    read resources from ` + "`" + `stdin` + "`" + ` and write the output to ` + "`" + `stdout` + "`" + `. When resources are
+    read from ` + "`" + `stdin` + "`" + `, they must be in one of the following input formats:
+  
+    1. Multi object YAML where resources are separated by ` + "`" + `---` + "`" + `.
+  
+    2. ` + "`" + `Function Specification` + "`" + ` wire format where resources are wrapped in an object
+       of kind ResourceList.
+   
+    If the output is written to ` + "`" + `stdout` + "`" + `, resources are written in multi object YAML
+    format where resources are separated by ` + "`" + `---` + "`" + `.
 
   fn-args:
     function arguments to be provided as input to the function. These must be
@@ -188,33 +197,44 @@ var RenderExamples = `
   $ kpt fn render my-package-dir
 `
 
-var SinkShort = `Specify a directory as an output sink package`
+var SinkShort = `Write resources to a local directory`
 var SinkLong = `
-  kpt fn sink [DIR]
+  kpt fn sink DIR [flags]
   
   DIR:
-    Path to a package directory.  Defaults to stdout if unspecified.
+    Path to a local directory to write resources to. Directory must exist.
 `
 var SinkExamples = `
-  # run a function using explicit sources and sinks
-  kpt fn source DIR/ |
-    kpt fn run --image gcr.io/example.com/my-fn |
-    kpt fn sink DIR/
+  # read resources from DIR directory, execute my-fn on them and write the
+  # output to DIR directory.
+  $ kpt fn source DIR |
+    kpt fn eval --image gcr.io/example.com/my-fn - |
+    kpt fn sink DIR
 `
 
-var SourceShort = `Specify a directory as an input source package`
+var SourceShort = `Source resources from a local directory`
 var SourceLong = `
-  kpt fn source [DIR...]
-  
+  kpt fn source [DIR] [flags]
+
+Args:
+
   DIR:
-    Path to a package directory.  Defaults to stdin if unspecified.
+    Path to the local directory containing resources. Defaults to the current
+    working directory.
+
+Flags:
+
+  --fn-config:
+    Path to the file containing ` + "`" + `functionConfig` + "`" + `.
+  
 `
 var SourceExamples = `
-  # print to stdout configuration from DIR/ formatted as an input source
-  kpt fn source DIR/
-
-  # run a function using explicit sources and sinks
-  kpt fn source DIR/ |
-    kpt fn run --image gcr.io/example.com/my-fn |
-    kpt fn sink DIR/
+  # read resources from DIR directory and write the output on stdout.
+  $ kpt fn source DIR
+
+  # read resources from DIR directory, execute my-fn on them and write the
+  # output to DIR directory.
+  $ kpt fn source DIR |
+    kpt fn eval --image gcr.io/example.com/my-fn - |
+    kpt fn sink DIR
 `
diff --git a/internal/errors/resolver/container.go b/internal/errors/resolver/container.go
new file mode 100644
index 00000000..6f2dedeb
--- /dev/null
+++ b/internal/errors/resolver/container.go
@@ -0,0 +1,41 @@
+// 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 (
+	"errors"
+
+	"github.com/GoogleContainerTools/kpt/internal/fnruntime"
+)
+
+//nolint:gochecknoinits
+func init() {
+	AddErrorResolver(&containerImageErrorResolver{})
+}
+
+// containerImageErrorResolver is an implementation of the ErrorResolver interface
+// to resolve container image errors.
+type containerImageErrorResolver struct{}
+
+func (*containerImageErrorResolver) Resolve(err error) (ResolvedResult, bool) {
+	var containerImageError *fnruntime.ContainerImageError
+	if !errors.As(err, &containerImageError) {
+		return ResolvedResult{}, false
+	}
+	return ResolvedResult{
+		Message:  containerImageError.Error(),
+		ExitCode: 1,
+	}, true
+}
diff --git a/internal/errors/resolver/live.go b/internal/errors/resolver/live.go
index a43c9bd1..5fb46953 100644
--- a/internal/errors/resolver/live.go
+++ b/internal/errors/resolver/live.go
@@ -15,6 +15,7 @@
 package resolver
 
 import (
+	"github.com/GoogleContainerTools/kpt/internal/cmdliveinit"
 	"github.com/GoogleContainerTools/kpt/internal/cmdutil"
 	"github.com/GoogleContainerTools/kpt/internal/errors"
 	"sigs.k8s.io/cli-utils/pkg/apply/taskrunner"
@@ -60,6 +61,11 @@ Error: Unable to install the ResourceGroup CRD.
 	//nolint:lll
 	noResourceGroupCRDMsg = `
 Error: The ResourceGroup CRD was not found in the cluster. Please install it either by using the '--install-resource-group' flag or the 'kpt live install-resource-group' command.
+`
+
+	//nolint:lll
+	invInfoAlreadyExistsMsg = `
+Error: Inventory information has already been added to the package Kptfile. Changing it after a package has been applied to the cluster can lead to undesired results. Use the --force flag to suppress this error.
 `
 
 	TimeoutErrorExitCode = 3
@@ -112,5 +118,13 @@ func (*liveErrorResolver) Resolve(err error) (ResolvedResult, bool) {
 		}, true
 	}
 
+	var invExistsError *cmdliveinit.InvExistsError
+	if errors.As(err, &invExistsError) {
+		return ResolvedResult{
+			Message:  ExecuteTemplate(invInfoAlreadyExistsMsg, tmplArgs),
+			ExitCode: 1,
+		}, true
+	}
+
 	return ResolvedResult{}, false
 }
diff --git a/internal/errors/resolver/pkg.go b/internal/errors/resolver/pkg.go
index d43fea84..c05079be 100644
--- a/internal/errors/resolver/pkg.go
+++ b/internal/errors/resolver/pkg.go
@@ -19,6 +19,7 @@ import (
 
 	"github.com/GoogleContainerTools/kpt/internal/errors"
 	"github.com/GoogleContainerTools/kpt/internal/pkg"
+	kptfile "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1alpha2"
 )
 
 //nolint:gochecknoinits
@@ -44,6 +45,7 @@ type pkgErrorResolver struct{}
 
 func (*pkgErrorResolver) Resolve(err error) (ResolvedResult, bool) {
 	var kptfileError *pkg.KptfileError
+	var validateError *kptfile.ValidateError
 	if errors.As(err, &kptfileError) {
 		path := kptfileError.Path
 		tmplArgs := map[string]interface{}{
@@ -63,6 +65,12 @@ func (*pkgErrorResolver) Resolve(err error) (ResolvedResult, bool) {
 			ExitCode: 1,
 		}, true
 	}
+	if errors.As(err, &validateError) {
+		return ResolvedResult{
+			Message:  validateError.Error(),
+			ExitCode: 1,
+		}, true
+	}
 
 	return ResolvedResult{}, false
 }
diff --git a/internal/fnruntime/container.go b/internal/fnruntime/container.go
index 595e8ac3..7a7daeae 100644
--- a/internal/fnruntime/container.go
+++ b/internal/fnruntime/container.go
@@ -163,7 +163,10 @@ func (f *ContainerFn) prepareImage() error {
 	var output []byte
 	var err error
 	if output, err = cmd.CombinedOutput(); err != nil {
-		return fmt.Errorf("failed to check local function image %q: %w", f.Image, err)
+		return &ContainerImageError{
+			Image:  f.Image,
+			Output: string(output),
+		}
 	}
 	if strings.Contains(string(output), strings.Split(f.Image, ":")[0]) {
 		// image exists locally
@@ -178,8 +181,22 @@ func (f *ContainerFn) prepareImage() error {
 	ctx, cancel := context.WithTimeout(context.Background(), timeout)
 	defer cancel()
 	cmd = exec.CommandContext(ctx, dockerBin, args...)
-	if _, err := cmd.CombinedOutput(); err != nil {
-		return fmt.Errorf("function image %q doesn't exist", f.Image)
+	if output, err := cmd.CombinedOutput(); err != nil {
+		return &ContainerImageError{
+			Image:  f.Image,
+			Output: string(output),
+		}
 	}
 	return nil
 }
+
+// ContainerImageError is an error type which will be returned when
+// the container run time cannot verify docker image.
+type ContainerImageError struct {
+	Image  string
+	Output string
+}
+
+func (e *ContainerImageError) Error() string {
+	return fmt.Sprintf("Function image %q doesn't exist.", e.Image)
+}
diff --git a/internal/fnruntime/runner.go b/internal/fnruntime/runner.go
index ff14e1d2..86623d78 100644
--- a/internal/fnruntime/runner.go
+++ b/internal/fnruntime/runner.go
@@ -20,6 +20,7 @@ import (
 	"fmt"
 	"io/ioutil"
 	"os"
+	"path"
 	"path/filepath"
 	"strings"
 
@@ -31,6 +32,7 @@ import (
 	"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"
 	"sigs.k8s.io/kustomize/kyaml/yaml"
 )
 
@@ -120,6 +122,9 @@ func (fr *FunctionRunner) do(input []*yaml.RNode) (output []*yaml.RNode, err err
 	fnResult := fr.fnResult
 
 	output, err = fr.filter.Filter(input)
+	if pathErr := enforcePathInvariants(output); pathErr != nil {
+		return output, pathErr
+	}
 
 	// parse the results irrespective of the success/failure of fn exec
 	resultErr := parseStructuredResult(fr.filter.Results, fnResult)
@@ -208,6 +213,33 @@ func printFnExecErr(ctx context.Context, fnErr *ExecError) {
 	pr.OptPrintf(printOpt.Stderr(), "  Exit code: %d\n\n", fnErr.ExitCode)
 }
 
+// path (location) of a KRM resources is tracked in a special key in
+// metadata.annotation field. enforcePathInvariants throws an error if there is a path
+// to a file outside the package, or if the same index/path is on multiple resources
+func enforcePathInvariants(nodes []*yaml.RNode) error {
+	// map has structure path -> index -> bool
+	// to keep track of paths and indexes found
+	pathIndexes := make(map[string]map[string]bool)
+	for _, node := range nodes {
+		currPath, index, err := kioutil.GetFileAnnotations(node)
+		if err != nil {
+			return err
+		}
+		fp := path.Clean(currPath)
+		if strings.HasPrefix(fp, "../") {
+			return fmt.Errorf("function must not modify resources outside of package: resource has path %s", currPath)
+		}
+		if pathIndexes[fp] == nil {
+			pathIndexes[fp] = make(map[string]bool)
+		}
+		if _, ok := pathIndexes[fp][index]; ok {
+			return fmt.Errorf("resource at path %q and index %q already exists", fp, index)
+		}
+		pathIndexes[fp][index] = true
+	}
+	return nil
+}
+
 // multiLineFormatter knows how to format multiple lines in pretty format
 // that can be displayed to an end user.
 type multiLineFormatter struct {
diff --git a/internal/util/addmergecomment/addmergecomment.go b/internal/util/addmergecomment/addmergecomment.go
index 4b6aff88..03a43f1c 100644
--- a/internal/util/addmergecomment/addmergecomment.go
+++ b/internal/util/addmergecomment/addmergecomment.go
@@ -1,3 +1,17 @@
+// 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 addmergecomment
 
 import (
diff --git a/internal/util/pkgutil/pkgutil.go b/internal/util/pkgutil/pkgutil.go
index c554d7a0..ed4f4e4a 100644
--- a/internal/util/pkgutil/pkgutil.go
+++ b/internal/util/pkgutil/pkgutil.go
@@ -24,7 +24,10 @@ import (
 
 	"github.com/GoogleContainerTools/kpt/internal/pkg"
 	kptfilev1alpha2 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1alpha2"
+	"github.com/GoogleContainerTools/kpt/pkg/kptfile/kptfileutil"
 	"sigs.k8s.io/kustomize/kyaml/copyutil"
+	"sigs.k8s.io/kustomize/kyaml/kio"
+	"sigs.k8s.io/kustomize/kyaml/kio/filters"
 )
 
 // WalkPackage walks the package defined at src and provides a callback for
@@ -273,6 +276,63 @@ func FindSubpackagesForPaths(matcher pkg.SubpackageMatcher, recurse bool, pkgPat
 	return paths, nil
 }
 
+// FormatPackage formats resources and meta-resources in the package and all its subpackages
+func FormatPackage(pkgPath string) {
+	inout := &kio.LocalPackageReadWriter{
+		PackagePath:    pkgPath,
+		MatchFilesGlob: append(kio.DefaultMatch, kptfilev1alpha2.KptFileName),
+	}
+	f := &filters.FormatFilter{
+		UseSchema: true,
+	}
+	err := kio.Pipeline{
+		Inputs:  []kio.Reader{inout},
+		Filters: []kio.Filter{f},
+		Outputs: []kio.Writer{inout},
+	}.Execute()
+	if err != nil {
+		// do not throw error if formatting fails
+		return
+	}
+	err = RoundTripKptfilesInPkg(pkgPath)
+	if err != nil {
+		// do not throw error if formatting fails
+		return
+	}
+}
+
+// RoundTripKptfilesInPkg reads and writes all Kptfiles in the package including
+// subpackages. This is used to format Kptfiles in the order of go structures
+// TODO: phanimarupaka remove this method after addressing https://github.com/GoogleContainerTools/kpt/issues/2052
+func RoundTripKptfilesInPkg(pkgPath string) error {
+	paths, err := pkg.Subpackages(pkgPath, pkg.All, true)
+	if err != nil {
+		return err
+	}
+
+	var pkgsPaths []string
+	for _, path := range paths {
+		// join pkgPath as the paths are relative to pkgPath
+		pkgsPaths = append(pkgsPaths, filepath.Join(pkgPath, path))
+	}
+	// include root package as well
+	pkgsPaths = append(pkgsPaths, pkgPath)
+
+	for _, pkgPath := range pkgsPaths {
+		kf, err := kptfileutil.ReadFile(pkgPath)
+		if err != nil {
+			// do not throw error if formatting fails
+			return err
+		}
+		err = kptfileutil.WriteFile(pkgPath, kf)
+		if err != nil {
+			// do not throw error if formatting fails
+			return err
+		}
+	}
+	return nil
+}
+
 // Exists returns true if a file or directory exists on the provided path,
 // and false otherwise.
 func Exists(path string) (bool, error) {
diff --git a/pkg/api/kptfile/v1alpha2/validation.go b/pkg/api/kptfile/v1alpha2/validation.go
index 579bc995..93db51de 100644
--- a/pkg/api/kptfile/v1alpha2/validation.go
+++ b/pkg/api/kptfile/v1alpha2/validation.go
@@ -11,6 +11,7 @@
 // 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 v1alpha2
 
 import (
@@ -41,12 +42,16 @@ func (p *Pipeline) validate() error {
 	if p == nil {
 		return nil
 	}
-	fns := []Function{}
-	fns = append(fns, p.Mutators...)
-	fns = append(fns, p.Validators...)
-	for i := range fns {
-		f := fns[i]
-		err := f.validate()
+	for i := range p.Mutators {
+		f := p.Mutators[i]
+		err := f.validate("mutators", i)
+		if err != nil {
+			return fmt.Errorf("function %q: %w", f.Image, err)
+		}
+	}
+	for i := range p.Validators {
+		f := p.Validators[i]
+		err := f.validate("validators", i)
 		if err != nil {
 			return fmt.Errorf("function %q: %w", f.Image, err)
 		}
@@ -54,16 +59,24 @@ func (p *Pipeline) validate() error {
 	return nil
 }
 
-func (f *Function) validate() error {
+func (f *Function) validate(fnType string, idx int) error {
 	err := validateFunctionName(f.Image)
 	if err != nil {
-		return fmt.Errorf("'image' is invalid: %w", err)
+		return &ValidateError{
+			Field:  fmt.Sprintf("pipeline.%s[%d].image", fnType, idx),
+			Value:  f.Image,
+			Reason: err.Error(),
+		}
 	}
 
 	var configFields []string
 	if f.ConfigPath != "" {
 		if err := validatePath(f.ConfigPath); err != nil {
-			return fmt.Errorf("'configPath' %q is invalid: %w", f.ConfigPath, err)
+			return &ValidateError{
+				Field:  fmt.Sprintf("pipeline.%s[%d].configPath", fnType, idx),
+				Value:  f.ConfigPath,
+				Reason: err.Error(),
+			}
 		}
 		configFields = append(configFields, "configPath")
 	}
@@ -73,13 +86,19 @@ func (f *Function) validate() error {
 	if !IsNodeZero(&f.Config) {
 		config := yaml.NewRNode(&f.Config)
 		if _, err := config.GetMeta(); err != nil {
-			return fmt.Errorf("functionConfig must be a valid KRM resource with `apiVersion` and `kind` fields")
+			return &ValidateError{
+				Field:  fmt.Sprintf("pipeline.%s[%d].config", fnType, idx),
+				Reason: "functionConfig must be a valid KRM resource with `apiVersion` and `kind` fields",
+			}
 		}
 		configFields = append(configFields, "config")
 	}
 	if len(configFields) > 1 {
-		return fmt.Errorf("following fields are mutually exclusive: 'config', 'configMap', 'configPath'. Got %q",
-			strings.Join(configFields, ", "))
+		return &ValidateError{
+			Field: fmt.Sprintf("pipeline.%s[%d]", fnType, idx),
+			Reason: fmt.Sprintf("only one of 'config', 'configMap', 'configPath' can be specified. Got %q",
+				strings.Join(configFields, ", ")),
+		}
 	}
 
 	return nil
@@ -148,3 +167,37 @@ func validatePath(p string) error {
 	}
 	return nil
 }
+
+// ValidateError is the error returned when validation fails.
+type ValidateError struct {
+	// Field is the field that causes error
+	Field string
+	// Value is the value of invalid field
+	Value string
+	// Reason is the reson for the error
+	Reason string
+}
+
+func (e *ValidateError) Error() string {
+	var sb strings.Builder
+	sb.WriteString(fmt.Sprintf("Kptfile is invalid:\nField: `%s`\n", e.Field))
+	if e.Value != "" {
+		sb.WriteString(fmt.Sprintf("Value: %q\n", e.Value))
+	}
+	sb.WriteString(fmt.Sprintf("Reason: %s\n", e.Reason))
+	return sb.String()
+}
+
+// Validate validates the fields in the inventory.
+func (i *Inventory) Validate() error {
+	if len(i.Name) == 0 {
+		return fmt.Errorf("name is required")
+	}
+	if len(i.Namespace) == 0 {
+		return fmt.Errorf("namespace is required")
+	}
+	if len(i.InventoryID) == 0 {
+		return fmt.Errorf("inventoryID is required")
+	}
+	return nil
+}
diff --git a/site/book/03-packages/02-exploring-a-package.md b/site/book/03-packages/02-exploring-a-package.md
index 0e675d1b..df097734 100644
--- a/site/book/03-packages/02-exploring-a-package.md
+++ b/site/book/03-packages/02-exploring-a-package.md
@@ -27,7 +27,7 @@ In addition, you can use a kpt function such as `search-replace` to run a query
 For example, to search for resources that have a field with path `spec.selector.tier`:
 
 ```shell
-$ kpt fn eval --image gcr.io/kpt-fn/search-replace:unstable wordpress -- 'by-path=spec.selector.tier'
+$ kpt fn eval wordpress --image gcr.io/kpt-fn/search-replace:v0.1 -- 'by-path=spec.selector.tier'
 ```
 
 [tree-doc]: /reference/pkg/tree/
diff --git a/site/book/03-packages/03-editing-a-package.md b/site/book/03-packages/03-editing-a-package.md
index 2989c766..bd8a9341 100644
--- a/site/book/03-packages/03-editing-a-package.md
+++ b/site/book/03-packages/03-editing-a-package.md
@@ -47,7 +47,7 @@ For example, setting a label on all the resources in the `wordpress` package can
 using the following function:
 
 ```shell
-$ kpt fn eval --image gcr.io/kpt-fn/set-labels:v0.1 wordpress -- env=dev
+$ kpt fn eval wordpress --image gcr.io/kpt-fn/set-labels:v0.1 -- env=dev
 ```
 
 [Chapter 4] discusses different ways of running functions in detail.
diff --git a/site/book/04-using-functions/01-declarative-function-execution.md b/site/book/04-using-functions/01-declarative-function-execution.md
index 9979a20e..2c567198 100644
--- a/site/book/04-using-functions/01-declarative-function-execution.md
+++ b/site/book/04-using-functions/01-declarative-function-execution.md
@@ -32,7 +32,7 @@ This declares two functions:
 - `set-label` is a mutator function which adds a set of labels to resources.
 - `kubeval` is a validator function which validates the resources against their OpenAPI schema.
 
-?> Refer to the [Functions Catalog](https://catalog.kpt.dev ':target=_self') for details on how to use a particular function.
+?> Refer to the [Functions Catalog](https://catalog.kpt.dev/ ':target=_self') for details on how to use a particular function.
 
 There are two differences between mutators and validators:
 
diff --git a/site/book/04-using-functions/02-imperative-function-execution.md b/site/book/04-using-functions/02-imperative-function-execution.md
index b67ce6e3..a704899c 100644
--- a/site/book/04-using-functions/02-imperative-function-execution.md
+++ b/site/book/04-using-functions/02-imperative-function-execution.md
@@ -4,7 +4,7 @@ This is referred to as imperative function execution.
 For example, to set the namespace of all resources in the wordpress package hierarchy:
 
 ```shell
-$ kpt fn eval --image gcr.io/kpt-fn/set-namespace:v0.1 wordpress -- namespace=mywordpress
+$ kpt fn eval wordpress --image gcr.io/kpt-fn/set-namespace:v0.1 -- namespace=mywordpress
 ```
 
 ?> Refer to the [eval command reference][eval-doc] for usage.
@@ -59,7 +59,7 @@ EOF
 ```
 
 ```shell
-$ kpt fn eval --image gcr.io/kpt-fn/set-namespace:v0.1 --fn-config /tmp/fn-config.yaml wordpress
+$ kpt fn eval wordpress --image gcr.io/kpt-fn/set-namespace:v0.1 --fn-config /tmp/fn-config.yaml
 ```
 
 ### CLI arguments
@@ -69,7 +69,7 @@ pairs as argument. For convenience, there is a way to provide the key/value pair
 arguments. The following is equivalent to what we showed previously:
 
 ```shell
-$ kpt fn eval --image gcr.io/kpt-fn/set-namespace:v0.1 wordpress -- namespace=mywordpress
+$ kpt fn eval wordpress --image gcr.io/kpt-fn/set-namespace:v0.1 -- namespace=mywordpress
 ```
 
 Note that the arguments must come after the separator `--`.
@@ -96,7 +96,7 @@ the `--network` flag.
 For example, `kubeval` function can download a JSON schema file:
 
 ```shell
-$ kpt fn eval --image gcr.io/kpt-fn/kubeval:v0.1 --network wordpress -- schema_location="https://kubernetesjsonschema.dev"
+$ kpt fn eval wordpress --image gcr.io/kpt-fn/kubeval:v0.1 --network -- schema_location="https://kubernetesjsonschema.dev"
 ```
 
 ### Mounting Directories
@@ -137,7 +137,7 @@ For instance, the following will set the labels on all resources in the `wordpre
 the `Kptfile`:
 
 ```shell
-$ kpt fn eval --image gcr.io/kpt-fn/set-labels:v0.1 --include-meta-resources wordpress -- app=wordpress env=prod
+$ kpt fn eval wordpress --image gcr.io/kpt-fn/set-labels:v0.1 --include-meta-resources -- app=wordpress env=prod
 ```
 
 ## Chaining functions using the Unix pipe
diff --git a/site/reference/fn/eval/README.md b/site/reference/fn/eval/README.md
index 83a47e8c..d5027053 100644
--- a/site/reference/fn/eval/README.md
+++ b/site/reference/fn/eval/README.md
@@ -22,20 +22,29 @@ Refer to the [Imperative Function Execution] for detailed overview.
 
 <!--mdtogo:Long-->
 
-```shell
+```
 kpt fn eval [DIR|-] [flags] [-- fn-args]
 ```
 
 #### Args
 
-```shell
+```
 DIR|-:
   Path to the local directory containing resources. Defaults to the current
   working directory. Using '-' as the directory path will cause `eval` to
-  read resources from `stdin` and write the output to `stdout`.
+  read resources from `stdin` and write the output to `stdout`. When resources are
+  read from `stdin`, they must be in one of the following input formats:
+
+  1. Multi object YAML where resources are separated by `---`.
+
+  2. `Function Specification` wire format where resources are wrapped in an object
+     of kind ResourceList.
+ 
+  If the output is written to `stdout`, resources are written in multi object YAML
+  format where resources are separated by `---`.
 ```
 
-```shell
+```
 fn-args:
   function arguments to be provided as input to the function. These must be
   provided in the `key=value` format and come after the separator `--`.
@@ -43,7 +52,7 @@ fn-args:
 
 #### Flags
 
-```shell
+```
 --as-current-user:
   Use the `uid` and `gid` of the kpt process for container function execution.
   By default, container function is executed as `nobody` user. You may want to use
@@ -98,55 +107,55 @@ fn-args:
 ## Examples
 <!--mdtogo:Examples-->
 
-```shell
+```
 # execute container my-fn on the resources in DIR directory and
 # write output back to DIR
 $ kpt fn eval DIR --image gcr.io/example.com/my-fn
 ```
 
-```shell
+```
 # execute container my-fn on the resources in DIR directory with
 # `functionConfig` my-fn-config
 $ kpt fn eval DIR --image gcr.io/example.com/my-fn --fn-config my-fn-config
 ```
 
-```shell
+```
 # execute container my-fn with an input ConfigMap containing `data: {foo: bar}`
 $ kpt fn eval DIR --image gcr.io/example.com/my-fn:v1.0.0 -- foo=bar
 ```
 
-```shell
+```
 # execute executable my-fn on the resources in DIR directory and
 # write output back to DIR
 $ kpt fn eval DIR --exec-path ./my-fn
 ```
 
-```shell
+```
 # execute container my-fn on the resources in DIR directory,
 # save structured results in /tmp/my-results dir and write output back to DIR
 $ kpt fn eval DIR --image gcr.io/example.com/my-fn --results-dir /tmp/my-results-dir
 ```
 
-```shell
+```
 # execute container my-fn on the resources in DIR directory with network access enabled,
 # and write output back to DIR
 $ kpt fn eval DIR --image gcr.io/example.com/my-fn --network 
 ```
 
-```shell
+```
 # execute container my-fn on the resource in DIR and export KUBECONFIG
 # and foo environment variable
 $ kpt fn eval DIR --image gcr.io/example.com/my-fn --env KUBECONFIG -e foo=bar
 ```
 
-```shell
+```
 # execute kubeval function by mounting schema from a local directory on wordpress package
 $ kpt fn eval --image gcr.io/kpt-fn/kubeval:v0.1 \
   --mount type=bind,src="/path/to/schema-dir",dst=/schema-dir \
   --as-current-user wordpress -- additional_schema_locations=/schema-dir
 ```
 
-```shell
+```
 # chaining functions using the unix pipe to set namespace and set labels on
 # wordpress package
 $ kpt fn source wordpress \
@@ -157,4 +166,5 @@ $ kpt fn source wordpress \
 <!--mdtogo-->
 
 [docker volumes]: https://docs.docker.com/storage/volumes/
-[Imperative Function Execution]: /book/04-using-functions/02-imperative-function-execution
\ No newline at end of file
+[Imperative Function Execution]: /book/04-using-functions/02-imperative-function-execution
+[Function Specification]: /book/05-developing-functions/02-function-specification
\ No newline at end of file
diff --git a/site/reference/fn/sink/README.md b/site/reference/fn/sink/README.md
index a2d8e673..d3d5665e 100644
--- a/site/reference/fn/sink/README.md
+++ b/site/reference/fn/sink/README.md
@@ -1,51 +1,52 @@
 ---
-title: "Sink"
+title: "`sink`"
 linkTitle: "sink"
 type: docs
 description: >
-   Specify a directory as an output sink package
+   Write resources to a local directory
 ---
 
 <!--mdtogo:Short
-    Specify a directory as an output sink package
+   Write resources to a local directory
 -->
 
-Implements a [sink function] by reading STDIN and writing configuration.
+`sink` reads resources from `stdin` and writes them to a local directory.
+Resources must be in one of the following input formats:
 
-Sink will not prune / delete files for delete resources because it only knows
-about files for which it sees input resources.
+  1. Multi object YAML where resources are separated by `---`.
 
-### Examples
-
-<!--mdtogo:Examples-->
+  2. `Function Specification` wire format where resources are wrapped in an object
+     of kind ResourceList.
 
-```shell
-# run a function using explicit sources and sinks
-kpt fn source DIR/ |
-  kpt fn run --image gcr.io/example.com/my-fn |
-  kpt fn sink DIR/
-```
-
-<!--mdtogo-->
+`sink` is useful for chaining functions using Unix pipe. For more details,
+refer to [Chaining functions].
 
 ### Synopsis
 
 <!--mdtogo:Long-->
 
-```shell
-kpt fn sink [DIR]
+```
+kpt fn sink DIR [flags]
 
 DIR:
-  Path to a package directory.  Defaults to stdout if unspecified.
+  Path to a local directory to write resources to. Directory must exist.
 ```
 
 <!--mdtogo-->
 
-### Next Steps
+### Examples
+
+<!--mdtogo:Examples-->
 
-- Learn about [functions concepts] like sources, sinks, and pipelines.
-- See more examples of sink functions in the functions [catalog].
+```
+# read resources from DIR directory, execute my-fn on them and write the
+# output to DIR directory.
+$ kpt fn source DIR |
+  kpt fn eval --image gcr.io/example.com/my-fn - |
+  kpt fn sink DIR
+```
+
+<!--mdtogo-->
 
-[sink function]: https://kpt.dev#todo
-[functions concepts]: /book/02-concepts/02-functions
-[catalog]: https://kpt.dev#todo
+[Chaining functions]: /book/04-using-functions/02-imperative-function-execution?id=chaining-functions-using-the-unix-pipe
+[Function Specification]: /book/05-developing-functions/02-function-specification
\ No newline at end of file
diff --git a/site/reference/fn/source/README.md b/site/reference/fn/source/README.md
index 8e31f3d0..3a2b6705 100644
--- a/site/reference/fn/source/README.md
+++ b/site/reference/fn/source/README.md
@@ -1,71 +1,64 @@
 ---
-title: "Source"
+title: "`source`"
 linkTitle: "source"
 type: docs
 description: >
-   Specify a directory as an input source package
+   Source resources from a local directory
 ---
 
 <!--mdtogo:Short
-    Specify a directory as an input source package
+    Source resources from a local directory
 -->
 
-Implements a [source function] by reading configuration and writing to STDOUT.
+`source` reads resources from a local directory and writes them in [Function Specification]
+wire format to `stdout`. The output of the `source` can be pipe'd to commands
+such as `kpt fn eval` that accepts [Function Specification] wire format. `source`
+is useful for chaining functions using Unix pipe. For more details, refer to
+[Chaining functions] and [Function Specification].
 
-### Examples
+### Synopsis
 
-{{% hide %}}
+<!--mdtogo:Long-->
 
-<!-- @makeWorkplace @verifyExamples-->
 ```
-# Set up workspace for the test.
-TEST_HOME=$(mktemp -d)
-cd $TEST_HOME
+kpt fn source [DIR] [flags]
 ```
 
-<!-- @fetchPackage @verifyExamples-->
-```shell
-export SRC_REPO=https://github.com/GoogleContainerTools/kpt.git
-kpt pkg get $SRC_REPO/package-examples/helloworld-set@next DIR/
-```
+#### Args
 
-{{% /hide %}}
+```
+DIR:
+  Path to the local directory containing resources. Defaults to the current
+  working directory.
+```
 
-<!--mdtogo:Examples-->
+#### Flags
 
-<!-- @fnSource @verifyExamples-->
-```shell
-# print to stdout configuration from DIR/ formatted as an input source
-kpt fn source DIR/
 ```
+--fn-config:
+  Path to the file containing `functionConfig`.
 
-```shell
-# run a function using explicit sources and sinks
-kpt fn source DIR/ |
-  kpt fn run --image gcr.io/example.com/my-fn |
-  kpt fn sink DIR/
 ```
 
 <!--mdtogo-->
+### Examples
 
-### Synopsis
-
-<!--mdtogo:Long-->
+<!--mdtogo:Examples-->
 
-```shell
-kpt fn source [DIR...]
+```
+# read resources from DIR directory and write the output on stdout.
+$ kpt fn source DIR
+```
 
-DIR:
-  Path to a package directory.  Defaults to stdin if unspecified.
+```
+# read resources from DIR directory, execute my-fn on them and write the
+# output to DIR directory.
+$ kpt fn source DIR |
+  kpt fn eval --image gcr.io/example.com/my-fn - |
+  kpt fn sink DIR
 ```
 
 <!--mdtogo-->
 
-### Next Steps
-
-- Learn about [functions concepts] like sources, sinks, and pipelines.
-- See more examples of source functions in the functions [catalog].
-
-[source function]: https://kpt.dev#todo
-[functions concepts]: /book/02-concepts/02-functions
-[catalog]: https://kpt.dev#todo
+[Chaining functions]: /book/04-using-functions/02-imperative-function-execution?id=chaining-functions-using-the-unix-pipe
+[Function Specification]: /book/05-developing-functions/02-function-specification
\ No newline at end of file
diff --git a/site/sidebar.md b/site/sidebar.md
index 725d1da0..a9d95bf9 100644
--- a/site/sidebar.md
+++ b/site/sidebar.md
@@ -50,6 +50,6 @@
         - [migrate](reference/live/migrate/)
         - [preview](reference/live/preview/)
         - [status](reference/live/status/)
-- [Functions Catalog](https://catalog.kpt.dev/ ':crossorgin :target=_self')
+- [Functions Catalog](https://catalog.kpt.dev/ ':target=_self')
 - [FAQ](faq/)
-- [Contact](contact/)
\ No newline at end of file
+- [Contact](contact/)
diff --git a/site/static/js/config.js b/site/static/js/config.js
index 047bb0ce..c6c8e7c6 100644
--- a/site/static/js/config.js
+++ b/site/static/js/config.js
@@ -11,7 +11,7 @@ window.$docsify = {
     "/*.*/sidebar.md": "/sidebar.md",
   },
   coverpage: "coverpage.md",
-  crossOriginLinks: ["https://catalog.kpt.dev"],
+  crossOriginLinks:["https://catalog.kpt.dev/"],
   auto2top: true,
   repo: "true",
   pagination: {
diff --git a/site/static/js/plugins.js b/site/static/js/plugins.js
index 069f1528..4ffdd50e 100644
--- a/site/static/js/plugins.js
+++ b/site/static/js/plugins.js
@@ -134,6 +134,20 @@ function processAsciinemaTags(content) {
   );
 }
 
+// Workaround for https://github.com/docsifyjs/docsify/pull/1468
+function defaultLinkTargets() {
+  const externalPageLinks = Array.from(
+    document.getElementsByTagName("a")
+  ).filter(
+    (a) =>
+      window.Docsify.util.isExternal(a.href) &&
+      !window.$docsify.crossOriginLinks.includes(a.href)
+  );
+  externalPageLinks.forEach(
+    (a) => (a.target = window.$docsify.externalLinkTarget)
+  );
+}
+
 function localPlugins(hook, _vm) {
   // Process Markdown directives appropriately.
   hook.beforeEach(function (content) {
@@ -151,6 +165,9 @@ function localPlugins(hook, _vm) {
   // Show navigation footer for book pages.
   hook.doneEach(showBookPageFooters);
 
+  // Reset all external links to their appropriate targets.
+  hook.doneEach(defaultLinkTargets);
+
   addGitHubWidget(hook);
 
   // Process elements in the navigation sidebar.
diff --git a/thirdparty/cli-utils/destroy/cmddestroy.go b/thirdparty/cli-utils/destroy/cmddestroy.go
deleted file mode 100644
index c647cb64..00000000
--- a/thirdparty/cli-utils/destroy/cmddestroy.go
+++ /dev/null
@@ -1,114 +0,0 @@
-// Copyright 2020 The Kubernetes Authors.
-// SPDX-License-Identifier: Apache-2.0
-
-package destroy
-
-import (
-	"fmt"
-	"strings"
-
-	"github.com/GoogleContainerTools/kpt/thirdparty/cli-utils/flagutils"
-	"github.com/GoogleContainerTools/kpt/thirdparty/cli-utils/printers"
-	"github.com/spf13/cobra"
-	"k8s.io/cli-runtime/pkg/genericclioptions"
-	cmdutil "k8s.io/kubectl/pkg/cmd/util"
-	"k8s.io/kubectl/pkg/util/i18n"
-	"sigs.k8s.io/cli-utils/pkg/apply"
-	"sigs.k8s.io/cli-utils/pkg/common"
-	"sigs.k8s.io/cli-utils/pkg/inventory"
-	"sigs.k8s.io/cli-utils/pkg/manifestreader"
-	"sigs.k8s.io/cli-utils/pkg/provider"
-)
-
-// GetDestroyRunner creates and returns the DestroyRunner which stores the cobra command.
-func GetDestroyRunner(provider provider.Provider, loader manifestreader.ManifestLoader, ioStreams genericclioptions.IOStreams) *DestroyRunner {
-	r := &DestroyRunner{
-		Destroyer: apply.NewDestroyer(provider),
-		ioStreams: ioStreams,
-		provider:  provider,
-		loader:    loader,
-	}
-	cmd := &cobra.Command{
-		Use:                   "destroy [PKG_PATH | -]",
-		DisableFlagsInUseLine: true,
-		Short:                 i18n.T("Destroy all the resources related to configuration"),
-		RunE:                  r.RunE,
-	}
-
-	cmd.Flags().StringVar(&r.output, "output", printers.DefaultPrinter(),
-		fmt.Sprintf("Output format, must be one of %s", strings.Join(printers.SupportedPrinters(), ",")))
-	cmd.Flags().StringVar(&r.inventoryPolicy, flagutils.InventoryPolicyFlag, flagutils.InventoryPolicyStrict,
-		"It determines the behavior when the resources don't belong to current inventory. Available options "+
-			fmt.Sprintf("%q and %q.", flagutils.InventoryPolicyStrict, flagutils.InventoryPolicyAdopt))
-
-	r.Command = cmd
-	return r
-}
-
-// DestroyCommand creates the DestroyRunner, returning the cobra command associated with it.
-func DestroyCommand(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
-	provider := provider.NewProvider(f)
-	loader := manifestreader.NewManifestLoader(f)
-	return GetDestroyRunner(provider, loader, ioStreams).Command
-}
-
-// DestroyRunner encapsulates data necessary to run the destroy command.
-type DestroyRunner struct {
-	Command    *cobra.Command
-	PreProcess func(info inventory.InventoryInfo, strategy common.DryRunStrategy) (inventory.InventoryPolicy, error)
-	ioStreams  genericclioptions.IOStreams
-	Destroyer  *apply.Destroyer
-	provider   provider.Provider
-	loader     manifestreader.ManifestLoader
-
-	output          string
-	inventoryPolicy string
-}
-
-func (r *DestroyRunner) RunE(cmd *cobra.Command, args []string) error {
-	if len(args) == 0 {
-		// default to the current working directory
-		args = append(args, ".")
-	}
-	inventoryPolicy, err := flagutils.ConvertInventoryPolicy(r.inventoryPolicy)
-	if err != nil {
-		return err
-	}
-
-	// Retrieve the inventory object.
-	reader, err := r.loader.ManifestReader(cmd.InOrStdin(), flagutils.PathFromArgs(args))
-	if err != nil {
-		return err
-	}
-	objs, err := reader.Read()
-	if err != nil {
-		return err
-	}
-	inv, _, err := r.loader.InventoryInfo(objs)
-	if err != nil {
-		return err
-	}
-
-	if r.PreProcess != nil {
-		inventoryPolicy, err = r.PreProcess(inv, r.Destroyer.DryRunStrategy)
-		if err != nil {
-			return err
-		}
-	}
-
-	// Run the destroyer. It will return a channel where we can receive updates
-	// to keep track of progress and any issues.
-	err = r.Destroyer.Initialize()
-	if err != nil {
-		return err
-	}
-	option := &apply.DestroyerOption{
-		InventoryPolicy: inventoryPolicy,
-	}
-	ch := r.Destroyer.Run(inv, option)
-
-	// The printer will print updates from the channel. It will block
-	// until the channel is closed.
-	printer := printers.GetPrinter(r.output, r.ioStreams)
-	return printer.Print(ch, r.Destroyer.DryRunStrategy)
-}
diff --git a/thirdparty/cmdconfig/commands/cmdeval/cmdeval.go b/thirdparty/cmdconfig/commands/cmdeval/cmdeval.go
index b5152b47..d37f64d7 100644
--- a/thirdparty/cmdconfig/commands/cmdeval/cmdeval.go
+++ b/thirdparty/cmdconfig/commands/cmdeval/cmdeval.go
@@ -12,6 +12,7 @@ import (
 
 	docs "github.com/GoogleContainerTools/kpt/internal/docs/generated/fndocs"
 	"github.com/GoogleContainerTools/kpt/internal/util/cmdutil"
+	"github.com/GoogleContainerTools/kpt/internal/util/pkgutil"
 	"github.com/GoogleContainerTools/kpt/thirdparty/cmdconfig/commands/runner"
 	"github.com/GoogleContainerTools/kpt/thirdparty/kyaml/runfn"
 	"github.com/spf13/cobra"
@@ -30,6 +31,7 @@ func GetEvalFnRunner(ctx context.Context, parent string) *EvalFnRunner {
 		Example: docs.EvalExamples,
 		RunE:    r.runE,
 		PreRunE: r.preRunE,
+		PostRun: r.postRun,
 	}
 
 	r.Command = c
@@ -293,3 +295,14 @@ func (r *EvalFnRunner) preRunE(c *cobra.Command, args []string) error {
 	// don't consider args for the function
 	return nil
 }
+
+func (r *EvalFnRunner) postRun(_ *cobra.Command, args []string) {
+	if len(args) > 0 && args[0] == "-" {
+		return
+	}
+	path := "."
+	if len(args) > 0 {
+		path = args[0]
+	}
+	pkgutil.FormatPackage(path)
+}
diff --git a/thirdparty/cmdconfig/commands/cmdsink/cmdsink.go b/thirdparty/cmdconfig/commands/cmdsink/cmdsink.go
index 567639df..0590cf3a 100644
--- a/thirdparty/cmdconfig/commands/cmdsink/cmdsink.go
+++ b/thirdparty/cmdconfig/commands/cmdsink/cmdsink.go
@@ -14,9 +14,9 @@ import (
 func GetSinkRunner(name string) *SinkRunner {
 	r := &SinkRunner{}
 	c := &cobra.Command{
-		Use:     "sink [DIR]",
+		Use:     "sink DIR [flags]",
 		Short:   fndocs.SinkShort,
-		Long:    fndocs.SinkLong,
+		Long:    fndocs.SinkShort + "\n" + fndocs.SinkLong,
 		Example: fndocs.SinkExamples,
 		RunE:    r.runE,
 		Args:    cobra.MinimumNArgs(1),
diff --git a/thirdparty/cmdconfig/commands/cmdsource/cmdsource.go b/thirdparty/cmdconfig/commands/cmdsource/cmdsource.go
index f93fc66d..0e4dfeb0 100644
--- a/thirdparty/cmdconfig/commands/cmdsource/cmdsource.go
+++ b/thirdparty/cmdconfig/commands/cmdsource/cmdsource.go
@@ -15,23 +15,22 @@ import (
 
 // GetSourceRunner returns a command for Source.
 func GetSourceRunner(name string) *SourceRunner {
-	r := &SourceRunner{}
+	r := &SourceRunner{
+		WrapKind:       kio.ResourceListKind,
+		WrapAPIVersion: kio.ResourceListAPIVersion,
+	}
 	c := &cobra.Command{
-		Use:     "source [DIR]",
+		Use:     "source [DIR] [flags]",
 		Short:   fndocs.SourceShort,
-		Long:    fndocs.SourceLong,
+		Long:    fndocs.SourceShort + "\n" + fndocs.SourceLong,
 		Example: fndocs.SourceExamples,
 		Args:    cobra.MaximumNArgs(1),
 		RunE:    r.runE,
 	}
-	c.Flags().StringVar(&r.WrapKind, "wrap-kind", kio.ResourceListKind,
-		"output using this format.")
-	c.Flags().StringVar(&r.WrapAPIVersion, "wrap-version", kio.ResourceListAPIVersion,
-		"output using this format.")
-	c.Flags().StringVar(&r.FunctionConfig, "function-config", "",
-		"path to function config.")
+	c.Flags().StringVar(&r.FunctionConfig, "fn-config", "",
+		"path to function config file.")
 	r.Command = c
-	_ = c.MarkFlagFilename("function-config", "yaml", "json", "yml")
+	_ = c.MarkFlagFilename("fn-config", "yaml", "json", "yml")
 	return r
 }
 


Test patch:
diff --git a/.github/workflows/e2eEnvironment.yml b/.github/workflows/e2eEnvironment.yml
new file mode 100644
index 00000000..9302fb09
--- /dev/null
+++ b/.github/workflows/e2eEnvironment.yml
@@ -0,0 +1,43 @@
+# 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.
+
+name: "Kpt Live - KinD Tests"
+on:
+  pull_request:
+    branches:
+      - next
+    paths-ignore:
+      - 'docs/**'
+      - 'site/**'
+
+jobs:
+  kind:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        version: [ 1.18, 1.19 ]
+    steps:
+      - uses: actions/checkout@master
+      # Pinned to Commit to ensure action is consistent: https://docs.github.com/en/actions/learn-github-actions/security-hardening-for-github-actions#using-third-party-actions
+      # If you upgrade this version confirm the changes match your expectations
+      - name: Install KinD
+        uses: engineerd/setup-kind@aa272fe2a7309878ffc2a81c56cfe3ef108ae7d0 # v0.5.0
+        with:
+          version: "v0.9.0"
+          skipClusterCreation: true
+      - name: Run Tests
+        env:
+          K8S_VERSION: ${{ matrix.version }}
+        run: |
+          ./e2e/live/end-to-end-test.sh -k $K8S_VERSION
\ No newline at end of file
diff --git a/e2e/live/end-to-end-test.sh b/e2e/live/end-to-end-test.sh
index 3374aa5a..0c1b2422 100755
--- a/e2e/live/end-to-end-test.sh
+++ b/e2e/live/end-to-end-test.sh
@@ -503,7 +503,7 @@ echo "Testing init Kptfile/ResourceGroup already initialized"
 echo "kpt live init e2e/live/testdata/rg-test-case-1a"
 ${BIN_DIR}/kpt live init e2e/live/testdata/rg-test-case-1a > $OUTPUT_DIR/status 2>&1
 assertContains "initializing Kptfile inventory info (namespace: rg-test-namespace)...failed"
-assertContains "error: ResourceGroup configuration has already been created."
+assertContains "Error: Inventory information has already been added to the package Kptfile."
 printResult
 
 echo "Testing init force Kptfile/ResourceGroup"
diff --git a/e2e/testdata/fn-eval/all-resource-deletion/starlark-delete-resource.yaml b/e2e/testdata/fn-eval/all-resource-deletion/starlark-delete-resource.yaml
index ba20bccf..fa3d0845 100644
--- a/e2e/testdata/fn-eval/all-resource-deletion/starlark-delete-resource.yaml
+++ b/e2e/testdata/fn-eval/all-resource-deletion/starlark-delete-resource.yaml
@@ -11,7 +11,6 @@
 # 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: fn.kpt.dev/v1alpha1
 kind: StarlarkRun
 metadata:
diff --git a/e2e/testdata/fn-eval/simple-function-consistent-formatting/.expected/config.yaml b/e2e/testdata/fn-eval/consistent-formatting/.expected/config.yaml
similarity index 96%
rename from e2e/testdata/fn-eval/simple-function-consistent-formatting/.expected/config.yaml
rename to e2e/testdata/fn-eval/consistent-formatting/.expected/config.yaml
index 20274aa3..710cb6f0 100644
--- a/e2e/testdata/fn-eval/simple-function-consistent-formatting/.expected/config.yaml
+++ b/e2e/testdata/fn-eval/consistent-formatting/.expected/config.yaml
@@ -14,5 +14,6 @@
 
 testType: eval
 image: gcr.io/kpt-fn/set-namespace:v0.1
+includeMetaResources: true
 args:
   namespace: staging
diff --git a/e2e/testdata/fn-eval/consistent-formatting/.expected/diff.patch b/e2e/testdata/fn-eval/consistent-formatting/.expected/diff.patch
new file mode 100644
index 00000000..5b210d26
--- /dev/null
+++ b/e2e/testdata/fn-eval/consistent-formatting/.expected/diff.patch
@@ -0,0 +1,69 @@
+diff --git a/Kptfile b/Kptfile
+index 74670e7..154c558 100644
+--- a/Kptfile
++++ b/Kptfile
+@@ -2,16 +2,7 @@ apiVersion: kpt.dev/v1alpha2
+ kind: Kptfile
+ metadata:
+   name: nginx
+-info:
+-  description: describe this package
+-  site: https://github.com/GoogleContainerTools/kpt
+-  emails:
+-    - foo@gmail.com
+-  license: license text
+-  keywords:
+-    - tag1
+-    - tag2
+-  man: nginx man text
++  namespace: staging
+ upstream:
+   type: git
+   git:
+@@ -26,6 +17,16 @@ upstreamLock:
+     directory: package-examples/nginx
+     ref: package-examples/nginx/v0.2
+     commit: 4d2aa98b45ddee4b5fa45fbca16f2ff887de9efb
++info:
++  site: https://github.com/GoogleContainerTools/kpt
++  emails:
++    - foo@gmail.com
++  license: license text
++  description: describe this package
++  keywords:
++    - tag1
++    - tag2
++  man: nginx man text
+ pipeline:
+   mutators:
+     - image: gcr.io/kpt-fn/set-namespace:v0.1
+diff --git a/resources.yaml b/resources.yaml
+index c0f48b0..9e31a48 100644
+--- a/resources.yaml
++++ b/resources.yaml
+@@ -14,9 +14,10 @@
+ apiVersion: apps/v1
+ kind: Deployment
+ metadata:
++  name: my-nginx
++  namespace: staging
+   labels:
+     foo: bar
+-  name: my-nginx
+ spec:
+   replicas: 3
+   selector:
+@@ -28,8 +29,8 @@ spec:
+         app: nginx
+     spec:
+       containers:
+-      - image: 'nginx:1.14.2'
+-        name: nginx
+-        ports:
+-        - containerPort: 80
+-          protocol: TCP
++        - name: nginx
++          image: nginx:1.14.2
++          ports:
++            - protocol: TCP
++              containerPort: 80
\ No newline at end of file
diff --git a/e2e/testdata/fn-eval/simple-function-consistent-formatting/.krmignore b/e2e/testdata/fn-eval/consistent-formatting/.krmignore
similarity index 100%
rename from e2e/testdata/fn-eval/simple-function-consistent-formatting/.krmignore
rename to e2e/testdata/fn-eval/consistent-formatting/.krmignore
diff --git a/e2e/testdata/fn-eval/consistent-formatting/Kptfile b/e2e/testdata/fn-eval/consistent-formatting/Kptfile
new file mode 100644
index 00000000..74670e7a
--- /dev/null
+++ b/e2e/testdata/fn-eval/consistent-formatting/Kptfile
@@ -0,0 +1,49 @@
+apiVersion: kpt.dev/v1alpha2
+kind: Kptfile
+metadata:
+  name: nginx
+info:
+  description: describe this package
+  site: https://github.com/GoogleContainerTools/kpt
+  emails:
+    - foo@gmail.com
+  license: license text
+  keywords:
+    - tag1
+    - tag2
+  man: nginx man text
+upstream:
+  type: git
+  git:
+    repo: https://github.com/GoogleContainerTools/kpt
+    directory: package-examples/nginx
+    ref: v0.2
+  updateStrategy: resource-merge
+upstreamLock:
+  type: git
+  git:
+    repo: https://github.com/GoogleContainerTools/kpt
+    directory: package-examples/nginx
+    ref: package-examples/nginx/v0.2
+    commit: 4d2aa98b45ddee4b5fa45fbca16f2ff887de9efb
+pipeline:
+  mutators:
+    - image: gcr.io/kpt-fn/set-namespace:v0.1
+      configMap:
+        namespace: some-space
+    - image: gcr.io/kpt-fn/apply-setters:v0.1
+      configMap:
+        image: nginx
+        tag: 1.14.1
+    - image: gcr.io/kpt-fn/set-labels:v0.1
+      configPath: ./fn-config.yaml
+    - image: gcr.io/kpt-fn/starlark:v0.1
+      configPath: path/to/foo-star.yaml
+inventory:
+  namespace: some-space
+  name: inventory-00933591
+  inventoryID: 92c234b7e9267815b0c3e17c9e4d7139a16c104f-1620493522822890000
+  labels:
+    foo: bar
+  annotations:
+    abc: def
diff --git a/e2e/testdata/fn-eval/simple-function-consistent-formatting/resources.yaml b/e2e/testdata/fn-eval/consistent-formatting/resources.yaml
similarity index 100%
rename from e2e/testdata/fn-eval/simple-function-consistent-formatting/resources.yaml
rename to e2e/testdata/fn-eval/consistent-formatting/resources.yaml
diff --git a/e2e/testdata/fn-eval/exclude-fn-config-by-default/Kptfile b/e2e/testdata/fn-eval/exclude-fn-config-by-default/Kptfile
index 3a55fd64..9d7c6396 100644
--- a/e2e/testdata/fn-eval/exclude-fn-config-by-default/Kptfile
+++ b/e2e/testdata/fn-eval/exclude-fn-config-by-default/Kptfile
@@ -4,5 +4,5 @@ metadata:
   name: app
 pipeline:
   mutators:
-  - image: gcr.io/kpt-fn/set-labels:v0.1
-    configPath: labelconfig.yaml
+    - image: gcr.io/kpt-fn/set-label:v0.1
+      configPath: labelconfig.yaml
diff --git a/e2e/testdata/fn-eval/exclude-fn-config-by-default/labelconfig.yaml b/e2e/testdata/fn-eval/exclude-fn-config-by-default/labelconfig.yaml
index 203c30cf..7ef98902 100644
--- a/e2e/testdata/fn-eval/exclude-fn-config-by-default/labelconfig.yaml
+++ b/e2e/testdata/fn-eval/exclude-fn-config-by-default/labelconfig.yaml
@@ -11,7 +11,6 @@
 # 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:
diff --git a/e2e/testdata/fn-eval/fn-config-file-in-pkg/.expected/diff.patch b/e2e/testdata/fn-eval/fn-config-file-in-pkg/.expected/diff.patch
index 3cff5539..1e6774f0 100644
--- a/e2e/testdata/fn-eval/fn-config-file-in-pkg/.expected/diff.patch
+++ b/e2e/testdata/fn-eval/fn-config-file-in-pkg/.expected/diff.patch
@@ -1,19 +1,14 @@
 diff --git a/config.yaml b/config.yaml
-index 3812e69..ea4fad1 100644
+index bb0bf5d..ea4fad1 100644
 --- a/config.yaml
 +++ b/config.yaml
-@@ -12,8 +12,9 @@
- # See the License for the specific language governing permissions and
- # limitations under the License.
- apiVersion: v1
--data:
--  namespace: staging
+@@ -15,5 +15,6 @@ apiVersion: v1
  kind: ConfigMap
  metadata:
    name: config
 +  namespace: staging
-+data:
-+  namespace: staging
+ data:
+   namespace: staging
 diff --git a/resources.yaml b/resources.yaml
 index 7a494c9..254b9cd 100644
 --- a/resources.yaml
@@ -32,4 +27,4 @@ index 7a494c9..254b9cd 100644
    name: custom
 +  namespace: staging
  spec:
-   image: nginx:1.2.3
+   image: nginx:1.2.3
\ No newline at end of file
diff --git a/e2e/testdata/fn-eval/fn-config-file-in-pkg/config.yaml b/e2e/testdata/fn-eval/fn-config-file-in-pkg/config.yaml
index 3812e69a..bb0bf5d6 100644
--- a/e2e/testdata/fn-eval/fn-config-file-in-pkg/config.yaml
+++ b/e2e/testdata/fn-eval/fn-config-file-in-pkg/config.yaml
@@ -12,8 +12,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 apiVersion: v1
-data:
-  namespace: staging
 kind: ConfigMap
 metadata:
   name: config
+data:
+  namespace: staging
diff --git a/e2e/testdata/fn-eval/fn-config-file/config.yaml b/e2e/testdata/fn-eval/fn-config-file/config.yaml
index ffa7b214..bb0bf5d6 100644
--- a/e2e/testdata/fn-eval/fn-config-file/config.yaml
+++ b/e2e/testdata/fn-eval/fn-config-file/config.yaml
@@ -11,7 +11,6 @@
 # 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:
diff --git a/e2e/testdata/fn-eval/function-chain/.expected/diff.patch b/e2e/testdata/fn-eval/function-chain/.expected/diff.patch
new file mode 100644
index 00000000..696e79d4
--- /dev/null
+++ b/e2e/testdata/fn-eval/function-chain/.expected/diff.patch
@@ -0,0 +1,30 @@
+diff --git a/resources.yaml b/resources.yaml
+index 7a494c9..0298ad0 100644
+--- a/resources.yaml
++++ b/resources.yaml
+@@ -15,12 +15,25 @@ apiVersion: apps/v1
+ kind: Deployment
+ metadata:
+   name: nginx-deployment
++  namespace: staging
++  labels:
++    foo: bar
+ spec:
+   replicas: 3
++  selector:
++    matchLabels:
++      foo: bar
++  template:
++    metadata:
++      labels:
++        foo: bar
+ ---
+ apiVersion: custom.io/v1
+ kind: Custom
+ metadata:
+   name: custom
++  namespace: staging
++  labels:
++    foo: bar
+ spec:
+   image: nginx:1.2.3
diff --git a/e2e/testdata/fn-eval/function-chain/.expected/exec.sh b/e2e/testdata/fn-eval/function-chain/.expected/exec.sh
new file mode 100644
index 00000000..1c7b0301
--- /dev/null
+++ b/e2e/testdata/fn-eval/function-chain/.expected/exec.sh
@@ -0,0 +1,6 @@
+#! /bin/bash
+
+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-chain/.krmignore b/e2e/testdata/fn-eval/function-chain/.krmignore
new file mode 100644
index 00000000..9d7a4007
--- /dev/null
+++ b/e2e/testdata/fn-eval/function-chain/.krmignore
@@ -0,0 +1,1 @@
+.expected
diff --git a/e2e/testdata/fn-eval/function-chain/resources.yaml b/e2e/testdata/fn-eval/function-chain/resources.yaml
new file mode 100644
index 00000000..7a494c9c
--- /dev/null
+++ b/e2e/testdata/fn-eval/function-chain/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/include-meta-resources/.expected/diff.patch b/e2e/testdata/fn-eval/include-meta-resources/.expected/diff.patch
index 24e005dd..1987bd93 100644
--- a/e2e/testdata/fn-eval/include-meta-resources/.expected/diff.patch
+++ b/e2e/testdata/fn-eval/include-meta-resources/.expected/diff.patch
@@ -1,34 +1,26 @@
 diff --git a/Kptfile b/Kptfile
-index 9c11c4f..f5353ca 100644
+index 9d7c639..7246298 100644
 --- a/Kptfile
 +++ b/Kptfile
-@@ -2,7 +2,8 @@ apiVersion: kpt.dev/v1alpha2
+@@ -2,6 +2,7 @@ apiVersion: kpt.dev/v1alpha2
  kind: Kptfile
  metadata:
    name: app
 +  namespace: staging
  pipeline:
    mutators:
--    - configPath: labelconfig.yaml
--      image: gcr.io/kpt-fn/set-labels:v0.1
-+    - image: gcr.io/kpt-fn/set-labels:v0.1
-+      configPath: labelconfig.yaml
+     - image: gcr.io/kpt-fn/set-label:v0.1
 diff --git a/labelconfig.yaml b/labelconfig.yaml
-index bb5d15c..f3a8b49 100644
+index 7ef9890..f3a8b49 100644
 --- a/labelconfig.yaml
 +++ b/labelconfig.yaml
-@@ -12,8 +12,9 @@
- # See the License for the specific language governing permissions and
- # limitations under the License.
- apiVersion: v1
--data:
--  tier: app
+@@ -15,5 +15,6 @@ apiVersion: v1
  kind: ConfigMap
  metadata:
    name: label-config
 +  namespace: staging
-+data:
-+  tier: app
+ data:
+   tier: app
 diff --git a/resources.yaml b/resources.yaml
 index 7a494c9..254b9cd 100644
 --- a/resources.yaml
@@ -47,4 +39,4 @@ index 7a494c9..254b9cd 100644
    name: custom
 +  namespace: staging
  spec:
-   image: nginx:1.2.3
+   image: nginx:1.2.3
\ No newline at end of file
diff --git a/e2e/testdata/fn-eval/include-meta-resources/Kptfile b/e2e/testdata/fn-eval/include-meta-resources/Kptfile
index 9c11c4f5..9d7c6396 100644
--- a/e2e/testdata/fn-eval/include-meta-resources/Kptfile
+++ b/e2e/testdata/fn-eval/include-meta-resources/Kptfile
@@ -4,5 +4,5 @@ metadata:
   name: app
 pipeline:
   mutators:
-    - configPath: labelconfig.yaml
-      image: gcr.io/kpt-fn/set-labels:v0.1
+    - image: gcr.io/kpt-fn/set-label:v0.1
+      configPath: labelconfig.yaml
diff --git a/e2e/testdata/fn-eval/include-meta-resources/labelconfig.yaml b/e2e/testdata/fn-eval/include-meta-resources/labelconfig.yaml
index bb5d15c4..7ef98902 100644
--- a/e2e/testdata/fn-eval/include-meta-resources/labelconfig.yaml
+++ b/e2e/testdata/fn-eval/include-meta-resources/labelconfig.yaml
@@ -12,8 +12,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 apiVersion: v1
-data:
-  tier: app
 kind: ConfigMap
 metadata:
   name: label-config
+data:
+  tier: app
diff --git a/e2e/testdata/fn-eval/invalid-fn-config-file/pkg/resources.yaml b/e2e/testdata/fn-eval/invalid-fn-config-file/pkg/resources.yaml
index 10cd18bf..7a494c9c 100644
--- a/e2e/testdata/fn-eval/invalid-fn-config-file/pkg/resources.yaml
+++ b/e2e/testdata/fn-eval/invalid-fn-config-file/pkg/resources.yaml
@@ -11,7 +11,6 @@
 # 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:
diff --git a/e2e/testdata/fn-eval/missing-fn-config/resources.yaml b/e2e/testdata/fn-eval/missing-fn-config/resources.yaml
index 10cd18bf..7a494c9c 100644
--- a/e2e/testdata/fn-eval/missing-fn-config/resources.yaml
+++ b/e2e/testdata/fn-eval/missing-fn-config/resources.yaml
@@ -11,7 +11,6 @@
 # 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:
diff --git a/e2e/testdata/fn-eval/missing-fn-image/.expected/config.yaml b/e2e/testdata/fn-eval/missing-fn-image/.expected/config.yaml
index bde03504..6093efcd 100644
--- a/e2e/testdata/fn-eval/missing-fn-image/.expected/config.yaml
+++ b/e2e/testdata/fn-eval/missing-fn-image/.expected/config.yaml
@@ -17,7 +17,7 @@ exitCode: 1
 image: gcr.io/kpt-fn/dne # non-existing image
 args:
   namespace: staging
-stdErr: "error: function image \"gcr.io/kpt-fn/dne\" doesn't exist"
+stdErr: 'Function image "gcr.io/kpt-fn/dne" doesn''t exist'
 stdOut: |
   [RUNNING] "gcr.io/kpt-fn/dne"
   [FAIL] "gcr.io/kpt-fn/dne"
diff --git a/e2e/testdata/fn-eval/missing-fn-image/resources.yaml b/e2e/testdata/fn-eval/missing-fn-image/resources.yaml
index 10cd18bf..7a494c9c 100644
--- a/e2e/testdata/fn-eval/missing-fn-image/resources.yaml
+++ b/e2e/testdata/fn-eval/missing-fn-image/resources.yaml
@@ -11,7 +11,6 @@
 # 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:
diff --git a/e2e/testdata/fn-eval/resource-deletion/starlark-delete-resource.yaml b/e2e/testdata/fn-eval/resource-deletion/starlark-delete-resource.yaml
index f485c801..f90d4de1 100644
--- a/e2e/testdata/fn-eval/resource-deletion/starlark-delete-resource.yaml
+++ b/e2e/testdata/fn-eval/resource-deletion/starlark-delete-resource.yaml
@@ -11,7 +11,6 @@
 # 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: fn.kpt.dev/v1alpha1
 kind: StarlarkRun
 metadata:
diff --git a/e2e/testdata/fn-eval/simple-function-consistent-formatting/.expected/diff.patch b/e2e/testdata/fn-eval/simple-function-consistent-formatting/.expected/diff.patch
deleted file mode 100644
index f59b17c8..00000000
--- a/e2e/testdata/fn-eval/simple-function-consistent-formatting/.expected/diff.patch
+++ /dev/null
@@ -1,30 +0,0 @@
-diff --git a/resources.yaml b/resources.yaml
-index c0f48b0..9e31a48 100644
---- a/resources.yaml
-+++ b/resources.yaml
-@@ -14,9 +14,10 @@
- apiVersion: apps/v1
- kind: Deployment
- metadata:
-+  name: my-nginx
-+  namespace: staging
-   labels:
-     foo: bar
--  name: my-nginx
- spec:
-   replicas: 3
-   selector:
-@@ -28,8 +29,8 @@ spec:
-         app: nginx
-     spec:
-       containers:
--      - image: 'nginx:1.14.2'
--        name: nginx
--        ports:
--        - containerPort: 80
--          protocol: TCP
-+        - name: nginx
-+          image: nginx:1.14.2
-+          ports:
-+            - protocol: TCP
-+              containerPort: 80
diff --git a/e2e/testdata/fn-eval/subpkg-exclude-fn-config-by-default/Kptfile b/e2e/testdata/fn-eval/subpkg-exclude-fn-config-by-default/Kptfile
index 3a55fd64..9d7c6396 100644
--- a/e2e/testdata/fn-eval/subpkg-exclude-fn-config-by-default/Kptfile
+++ b/e2e/testdata/fn-eval/subpkg-exclude-fn-config-by-default/Kptfile
@@ -4,5 +4,5 @@ metadata:
   name: app
 pipeline:
   mutators:
-  - image: gcr.io/kpt-fn/set-labels:v0.1
-    configPath: labelconfig.yaml
+    - image: gcr.io/kpt-fn/set-label:v0.1
+      configPath: labelconfig.yaml
diff --git a/e2e/testdata/fn-eval/subpkg-exclude-fn-config-by-default/db/Kptfile b/e2e/testdata/fn-eval/subpkg-exclude-fn-config-by-default/db/Kptfile
index ecd1c5f0..94a14166 100644
--- a/e2e/testdata/fn-eval/subpkg-exclude-fn-config-by-default/db/Kptfile
+++ b/e2e/testdata/fn-eval/subpkg-exclude-fn-config-by-default/db/Kptfile
@@ -4,5 +4,5 @@ metadata:
   name: db
 pipeline:
   mutators:
-  - image: gcr.io/kpt-fn/set-labels:unstable
-    configPath: labelconfig.yaml
+    - image: gcr.io/kpt-fn/set-label:unstable
+      configPath: labelconfig.yaml
diff --git a/e2e/testdata/fn-eval/subpkg-exclude-fn-config-by-default/labelconfig.yaml b/e2e/testdata/fn-eval/subpkg-exclude-fn-config-by-default/labelconfig.yaml
index 203c30cf..7ef98902 100644
--- a/e2e/testdata/fn-eval/subpkg-exclude-fn-config-by-default/labelconfig.yaml
+++ b/e2e/testdata/fn-eval/subpkg-exclude-fn-config-by-default/labelconfig.yaml
@@ -11,7 +11,6 @@
 # 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:
diff --git a/e2e/testdata/fn-eval/subpkg-include-meta-resources/.expected/diff.patch b/e2e/testdata/fn-eval/subpkg-include-meta-resources/.expected/diff.patch
index 07c9acd5..088144fc 100644
--- a/e2e/testdata/fn-eval/subpkg-include-meta-resources/.expected/diff.patch
+++ b/e2e/testdata/fn-eval/subpkg-include-meta-resources/.expected/diff.patch
@@ -1,49 +1,38 @@
 diff --git a/Kptfile b/Kptfile
-index 9c11c4f..f5353ca 100644
+index 9d7c639..7246298 100644
 --- a/Kptfile
 +++ b/Kptfile
-@@ -2,7 +2,8 @@ apiVersion: kpt.dev/v1alpha2
+@@ -2,6 +2,7 @@ apiVersion: kpt.dev/v1alpha2
  kind: Kptfile
  metadata:
    name: app
 +  namespace: staging
  pipeline:
    mutators:
--    - configPath: labelconfig.yaml
--      image: gcr.io/kpt-fn/set-labels:v0.1
-+    - image: gcr.io/kpt-fn/set-labels:v0.1
-+      configPath: labelconfig.yaml
+     - image: gcr.io/kpt-fn/set-label:v0.1
 diff --git a/db/Kptfile b/db/Kptfile
-index 94fe38b..462c556 100644
+index 94a1416..64ade60 100644
 --- a/db/Kptfile
 +++ b/db/Kptfile
-@@ -2,7 +2,8 @@ apiVersion: kpt.dev/v1alpha2
+@@ -2,6 +2,7 @@ apiVersion: kpt.dev/v1alpha2
  kind: Kptfile
  metadata:
    name: db
 +  namespace: staging
  pipeline:
    mutators:
--    - configPath: labelconfig.yaml
--      image: gcr.io/kpt-fn/set-labels:unstable
-+    - image: gcr.io/kpt-fn/set-labels:unstable
-+      configPath: labelconfig.yaml
+     - image: gcr.io/kpt-fn/set-label:unstable
 diff --git a/db/labelconfig.yaml b/db/labelconfig.yaml
-index ad6b09d..b7b0335 100644
+index fec6885..b7b0335 100644
 --- a/db/labelconfig.yaml
 +++ b/db/labelconfig.yaml
-@@ -12,8 +12,9 @@
- # See the License for the specific language governing permissions and
- # limitations under the License.
- apiVersion: v1
--data:
--  namespace: db
+@@ -15,5 +15,6 @@ apiVersion: v1
  kind: ConfigMap
  metadata:
    name: db-label-config
 +  namespace: staging
-+data:
-+  namespace: db
+ data:
+   namespace: db
 diff --git a/db/resources.yaml b/db/resources.yaml
 index ac1fd96..64ec0ee 100644
 --- a/db/resources.yaml
@@ -56,21 +45,16 @@ index ac1fd96..64ec0ee 100644
  spec:
    replicas: 3
 diff --git a/labelconfig.yaml b/labelconfig.yaml
-index bb5d15c..f3a8b49 100644
+index 7ef9890..f3a8b49 100644
 --- a/labelconfig.yaml
 +++ b/labelconfig.yaml
-@@ -12,8 +12,9 @@
- # See the License for the specific language governing permissions and
- # limitations under the License.
- apiVersion: v1
--data:
--  tier: app
+@@ -15,5 +15,6 @@ apiVersion: v1
  kind: ConfigMap
  metadata:
    name: label-config
 +  namespace: staging
-+data:
-+  tier: app
+ data:
+   tier: app
 diff --git a/resources.yaml b/resources.yaml
 index 7a494c9..254b9cd 100644
 --- a/resources.yaml
@@ -89,4 +73,4 @@ index 7a494c9..254b9cd 100644
    name: custom
 +  namespace: staging
  spec:
-   image: nginx:1.2.3
+   image: nginx:1.2.3
\ No newline at end of file
diff --git a/e2e/testdata/fn-eval/subpkg-include-meta-resources/Kptfile b/e2e/testdata/fn-eval/subpkg-include-meta-resources/Kptfile
index 9c11c4f5..9d7c6396 100644
--- a/e2e/testdata/fn-eval/subpkg-include-meta-resources/Kptfile
+++ b/e2e/testdata/fn-eval/subpkg-include-meta-resources/Kptfile
@@ -4,5 +4,5 @@ metadata:
   name: app
 pipeline:
   mutators:
-    - configPath: labelconfig.yaml
-      image: gcr.io/kpt-fn/set-labels:v0.1
+    - image: gcr.io/kpt-fn/set-label:v0.1
+      configPath: labelconfig.yaml
diff --git a/e2e/testdata/fn-eval/subpkg-include-meta-resources/db/Kptfile b/e2e/testdata/fn-eval/subpkg-include-meta-resources/db/Kptfile
index 94fe38b4..94a14166 100644
--- a/e2e/testdata/fn-eval/subpkg-include-meta-resources/db/Kptfile
+++ b/e2e/testdata/fn-eval/subpkg-include-meta-resources/db/Kptfile
@@ -4,5 +4,5 @@ metadata:
   name: db
 pipeline:
   mutators:
-    - configPath: labelconfig.yaml
-      image: gcr.io/kpt-fn/set-labels:unstable
+    - image: gcr.io/kpt-fn/set-label:unstable
+      configPath: labelconfig.yaml
diff --git a/e2e/testdata/fn-eval/subpkg-include-meta-resources/db/labelconfig.yaml b/e2e/testdata/fn-eval/subpkg-include-meta-resources/db/labelconfig.yaml
index ad6b09df..fec6885a 100644
--- a/e2e/testdata/fn-eval/subpkg-include-meta-resources/db/labelconfig.yaml
+++ b/e2e/testdata/fn-eval/subpkg-include-meta-resources/db/labelconfig.yaml
@@ -12,8 +12,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 apiVersion: v1
-data:
-  namespace: db
 kind: ConfigMap
 metadata:
   name: db-label-config
+data:
+  namespace: db
diff --git a/e2e/testdata/fn-eval/subpkg-include-meta-resources/labelconfig.yaml b/e2e/testdata/fn-eval/subpkg-include-meta-resources/labelconfig.yaml
index bb5d15c4..7ef98902 100644
--- a/e2e/testdata/fn-eval/subpkg-include-meta-resources/labelconfig.yaml
+++ b/e2e/testdata/fn-eval/subpkg-include-meta-resources/labelconfig.yaml
@@ -12,8 +12,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 apiVersion: v1
-data:
-  tier: app
 kind: ConfigMap
 metadata:
   name: label-config
+data:
+  tier: app
diff --git a/e2e/testdata/fn-render/all-resource-deletion/.expected/diff.patch b/e2e/testdata/fn-render/all-resource-deletion/.expected/diff.patch
index 097c4778..845feae6 100644
--- a/e2e/testdata/fn-render/all-resource-deletion/.expected/diff.patch
+++ b/e2e/testdata/fn-render/all-resource-deletion/.expected/diff.patch
@@ -1,9 +1,9 @@
 diff --git a/deployment.yaml b/deployment.yaml
 deleted file mode 100644
-index 6cfff34..0000000
+index 737526e..0000000
 --- a/deployment.yaml
 +++ /dev/null
-@@ -1,25 +0,0 @@
+@@ -1,24 +0,0 @@
 -# Copyright 2021 Google LLC
 -#
 -# Licensed under the Apache License, Version 2.0 (the "License");
@@ -17,7 +17,6 @@ index 6cfff34..0000000
 -# 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:
@@ -27,14 +26,14 @@ index 6cfff34..0000000
 -  template:
 -    spec:
 -      containers:
--      - image: kennethreitz/httpbin
--        name: httpbin
+-        - name: httpbin
+-          image: kennethreitz/httpbin
 diff --git a/resources.yaml b/resources.yaml
 deleted file mode 100644
-index cf4a2cf..0000000
+index 6590f6c..0000000
 --- a/resources.yaml
 +++ /dev/null
-@@ -1,20 +0,0 @@
+@@ -1,19 +0,0 @@
 -# Copyright 2021 Google LLC
 -#
 -# Licensed under the Apache License, Version 2.0 (the "License");
@@ -48,10 +47,9 @@ index cf4a2cf..0000000
 -# 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
+-  replicas: 3
\ No newline at end of file
diff --git a/e2e/testdata/fn-render/all-resource-deletion/Kptfile b/e2e/testdata/fn-render/all-resource-deletion/Kptfile
index 6e24daa9..9768f882 100644
--- a/e2e/testdata/fn-render/all-resource-deletion/Kptfile
+++ b/e2e/testdata/fn-render/all-resource-deletion/Kptfile
@@ -4,11 +4,11 @@ metadata:
   name: app
 pipeline:
   mutators:
-  - image: gcr.io/kpt-fn/starlark:unstable
-    configPath: delete-all.yaml
-  - image: gcr.io/kpt-fn/set-namespace:unstable
-    configMap:
-      namespace: staging
-  - image: gcr.io/kpt-fn/set-labels:unstable
-    configMap:
-      tier: backend
+    - image: gcr.io/kpt-fn/starlark:unstable
+      configPath: delete-all.yaml
+    - image: gcr.io/kpt-fn/set-namespace:unstable
+      configMap:
+        namespace: staging
+    - image: gcr.io/kpt-fn/set-label:unstable
+      configMap:
+        tier: backend
diff --git a/e2e/testdata/fn-render/all-resource-deletion/delete-all.yaml b/e2e/testdata/fn-render/all-resource-deletion/delete-all.yaml
index 2f3e2eb2..841a88d7 100644
--- a/e2e/testdata/fn-render/all-resource-deletion/delete-all.yaml
+++ b/e2e/testdata/fn-render/all-resource-deletion/delete-all.yaml
@@ -11,7 +11,6 @@
 # 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: fn.kpt.dev/v1alpha1
 kind: StarlarkRun
 metadata:
diff --git a/e2e/testdata/fn-render/all-resource-deletion/deployment.yaml b/e2e/testdata/fn-render/all-resource-deletion/deployment.yaml
index 6cfff34f..737526eb 100644
--- a/e2e/testdata/fn-render/all-resource-deletion/deployment.yaml
+++ b/e2e/testdata/fn-render/all-resource-deletion/deployment.yaml
@@ -11,7 +11,6 @@
 # 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:
@@ -21,5 +20,5 @@ spec:
   template:
     spec:
       containers:
-      - image: kennethreitz/httpbin
-        name: httpbin
+        - name: httpbin
+          image: kennethreitz/httpbin
diff --git a/e2e/testdata/fn-render/all-resource-deletion/resources.yaml b/e2e/testdata/fn-render/all-resource-deletion/resources.yaml
index cf4a2cf0..6590f6ca 100644
--- a/e2e/testdata/fn-render/all-resource-deletion/resources.yaml
+++ b/e2e/testdata/fn-render/all-resource-deletion/resources.yaml
@@ -11,7 +11,6 @@
 # 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:
diff --git a/e2e/testdata/fn-render/basicpipeline/Kptfile b/e2e/testdata/fn-render/basicpipeline/Kptfile
index 2b15e8f9..1a5dd33c 100644
--- a/e2e/testdata/fn-render/basicpipeline/Kptfile
+++ b/e2e/testdata/fn-render/basicpipeline/Kptfile
@@ -4,9 +4,9 @@ metadata:
   name: app
 pipeline:
   mutators:
-  - image: gcr.io/kpt-fn/set-namespace:unstable
-    configMap:
-      namespace: staging
-  - image: gcr.io/kpt-fn/set-labels:unstable
-    configMap:
-      tier: backend
\ No newline at end of file
+    - image: gcr.io/kpt-fn/set-namespace:unstable
+      configMap:
+        namespace: staging
+    - image: gcr.io/kpt-fn/set-label:unstable
+      configMap:
+        tier: backend
diff --git a/e2e/testdata/fn-render/empty-pipeline/Kptfile b/e2e/testdata/fn-render/empty-pipeline/Kptfile
index ea9a3b73..30dcdc95 100644
--- a/e2e/testdata/fn-render/empty-pipeline/Kptfile
+++ b/e2e/testdata/fn-render/empty-pipeline/Kptfile
@@ -2,4 +2,4 @@ apiVersion: kpt.dev/v1alpha2
 kind: Kptfile
 metadata:
   name: app
-pipeline:
\ No newline at end of file
+pipeline:
diff --git a/e2e/testdata/fn-render/empty-pipeline/resources.yaml b/e2e/testdata/fn-render/empty-pipeline/resources.yaml
index 10cd18bf..7a494c9c 100644
--- a/e2e/testdata/fn-render/empty-pipeline/resources.yaml
+++ b/e2e/testdata/fn-render/empty-pipeline/resources.yaml
@@ -11,7 +11,6 @@
 # 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:
diff --git a/e2e/testdata/fn-render/fn-failure-output-no-truncate/Kptfile b/e2e/testdata/fn-render/fn-failure-output-no-truncate/Kptfile
index 2909caf6..b1d95459 100644
--- a/e2e/testdata/fn-render/fn-failure-output-no-truncate/Kptfile
+++ b/e2e/testdata/fn-render/fn-failure-output-no-truncate/Kptfile
@@ -4,6 +4,6 @@ metadata:
   name: app
 pipeline:
   mutators:
-  - image: gcr.io/kpt-fn/kubeval:v0.1
-    configMap:
-      strict: "true"
+    - image: gcr.io/kpt-fn/kubeval:v0.1
+      configMap:
+        strict: "true"
diff --git a/e2e/testdata/fn-render/fn-failure-output-no-truncate/resources.yaml b/e2e/testdata/fn-render/fn-failure-output-no-truncate/resources.yaml
index 9ccaaa45..085379f3 100644
--- a/e2e/testdata/fn-render/fn-failure-output-no-truncate/resources.yaml
+++ b/e2e/testdata/fn-render/fn-failure-output-no-truncate/resources.yaml
@@ -11,7 +11,6 @@
 # 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:
diff --git a/e2e/testdata/fn-render/fn-failure/resources.yaml b/e2e/testdata/fn-render/fn-failure/resources.yaml
index 10cd18bf..7a494c9c 100644
--- a/e2e/testdata/fn-render/fn-failure/resources.yaml
+++ b/e2e/testdata/fn-render/fn-failure/resources.yaml
@@ -11,7 +11,6 @@
 # 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:
diff --git a/e2e/testdata/fn-render/fnconfig/Kptfile b/e2e/testdata/fn-render/fnconfig/Kptfile
index 5bb26b06..c61850f6 100644
--- a/e2e/testdata/fn-render/fnconfig/Kptfile
+++ b/e2e/testdata/fn-render/fnconfig/Kptfile
@@ -4,8 +4,8 @@ metadata:
   name: app-with-db
 pipeline:
   mutators:
-  - image: gcr.io/kpt-fn/set-namespace:unstable
-    configMap:
-      namespace: staging
-  - image: gcr.io/kpt-fn/set-labels:unstable
-    configPath: labelconfig.yaml
\ No newline at end of file
+    - image: gcr.io/kpt-fn/set-namespace:unstable
+      configMap:
+        namespace: staging
+    - image: gcr.io/kpt-fn/set-label:unstable
+      configPath: labelconfig.yaml
diff --git a/e2e/testdata/fn-render/fnconfig/db/Kptfile b/e2e/testdata/fn-render/fnconfig/db/Kptfile
index 0396b671..91e81375 100644
--- a/e2e/testdata/fn-render/fnconfig/db/Kptfile
+++ b/e2e/testdata/fn-render/fnconfig/db/Kptfile
@@ -4,9 +4,9 @@ metadata:
   name: db
 pipeline:
   mutators:
-  - image: gcr.io/kpt-fn/set-namespace:unstable
-    configMap:
-      namespace: db
-  - image: gcr.io/kpt-fn/set-labels:unstable
-    configMap:
-      app: backend
\ No newline at end of file
+    - image: gcr.io/kpt-fn/set-namespace:unstable
+      configMap:
+        namespace: db
+    - image: gcr.io/kpt-fn/set-label:unstable
+      configMap:
+        app: backend
diff --git a/e2e/testdata/fn-render/fnresult-fn-failure/Kptfile b/e2e/testdata/fn-render/fnresult-fn-failure/Kptfile
index 2909caf6..b1d95459 100644
--- a/e2e/testdata/fn-render/fnresult-fn-failure/Kptfile
+++ b/e2e/testdata/fn-render/fnresult-fn-failure/Kptfile
@@ -4,6 +4,6 @@ metadata:
   name: app
 pipeline:
   mutators:
-  - image: gcr.io/kpt-fn/kubeval:v0.1
-    configMap:
-      strict: "true"
+    - image: gcr.io/kpt-fn/kubeval:v0.1
+      configMap:
+        strict: "true"
diff --git a/e2e/testdata/fn-render/fnresult-fn-failure/resources.yaml b/e2e/testdata/fn-render/fnresult-fn-failure/resources.yaml
index 9ccaaa45..085379f3 100644
--- a/e2e/testdata/fn-render/fnresult-fn-failure/resources.yaml
+++ b/e2e/testdata/fn-render/fnresult-fn-failure/resources.yaml
@@ -11,7 +11,6 @@
 # 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:
diff --git a/e2e/testdata/fn-render/fnresult-fn-success/Kptfile b/e2e/testdata/fn-render/fnresult-fn-success/Kptfile
index b6396901..4c98a1ae 100644
--- a/e2e/testdata/fn-render/fnresult-fn-success/Kptfile
+++ b/e2e/testdata/fn-render/fnresult-fn-success/Kptfile
@@ -4,5 +4,5 @@ metadata:
   name: app
 pipeline:
   mutators:
-  - image: gcr.io/kpt-fn/search-replace:v0.1
-    configPath: search-replace-conf.yaml
+    - image: gcr.io/kpt-fn/search-replace:v0.1
+      configPath: search-replace-conf.yaml
diff --git a/e2e/testdata/fn-render/fnresult-fn-success/search-replace-conf.yaml b/e2e/testdata/fn-render/fnresult-fn-success/search-replace-conf.yaml
index fde1e691..34be0f65 100644
--- a/e2e/testdata/fn-render/fnresult-fn-success/search-replace-conf.yaml
+++ b/e2e/testdata/fn-render/fnresult-fn-success/search-replace-conf.yaml
@@ -11,12 +11,11 @@
 # 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: search-replace-fn-config
 data:
   by-path: spec.replicas
-  by-value: 3
-  put-value: 4
+  by-value: "3"
+  put-value: "4"
diff --git a/e2e/testdata/fn-render/format-on-success/.expected/diff.patch b/e2e/testdata/fn-render/format-on-success/.expected/diff.patch
index ade22aca..0224d15e 100644
--- a/e2e/testdata/fn-render/format-on-success/.expected/diff.patch
+++ b/e2e/testdata/fn-render/format-on-success/.expected/diff.patch
@@ -1,5 +1,5 @@
 diff --git a/db/resources.yaml b/db/resources.yaml
-index e405a52..64ec0ee 100644
+index ac1fd96..64ec0ee 100644
 --- a/db/resources.yaml
 +++ b/db/resources.yaml
 @@ -15,5 +15,6 @@ apiVersion: apps/v1
@@ -8,13 +8,12 @@ index e405a52..64ec0ee 100644
    name: db
 +  namespace: staging
  spec:
--    replicas: 3
-+  replicas: 3
+   replicas: 3
 diff --git a/resources.yaml b/resources.yaml
-index 71cb6e1..cb937aa 100644
+index 317f422..cb937aa 100644
 --- a/resources.yaml
 +++ b/resources.yaml
-@@ -15,11 +15,12 @@ apiVersion: apps/v1
+@@ -15,6 +15,7 @@ apiVersion: apps/v1
  kind: Deployment
  metadata:
    name: nginx-deployment
@@ -22,18 +21,3 @@ index 71cb6e1..cb937aa 100644
  spec:
    replicas: 3
    selector:
--      matchLabels:
--        app: nginx
-+    matchLabels:
-+      app: nginx
-   template:
-     metadata:
-       labels:
-@@ -27,6 +28,6 @@ spec:
-     spec:
-       containers:
-         - name: nginx
-+          image: nginx:1.14.2
-           ports:
-             - containerPort: 80
--          image: nginx:1.14.2
diff --git a/e2e/testdata/fn-render/format-on-success/Kptfile b/e2e/testdata/fn-render/format-on-success/Kptfile
index 8b7bdb0a..c759bf1e 100644
--- a/e2e/testdata/fn-render/format-on-success/Kptfile
+++ b/e2e/testdata/fn-render/format-on-success/Kptfile
@@ -4,6 +4,6 @@ metadata:
   name: app-with-db
 pipeline:
   mutators:
-  - image: gcr.io/kpt-fn/set-namespace:unstable
-    configMap:
-      namespace: staging
+    - image: gcr.io/kpt-fn/set-namespace:unstable
+      configMap:
+        namespace: staging
diff --git a/e2e/testdata/fn-render/format-on-success/db/Kptfile b/e2e/testdata/fn-render/format-on-success/db/Kptfile
index 5f58d54f..cb0de4ab 100644
--- a/e2e/testdata/fn-render/format-on-success/db/Kptfile
+++ b/e2e/testdata/fn-render/format-on-success/db/Kptfile
@@ -4,6 +4,6 @@ metadata:
   name: db
 pipeline:
   mutators:
-  - image: gcr.io/kpt-fn/set-namespace:unstable
-    configMap:
-      namespace: db
+    - image: gcr.io/kpt-fn/set-namespace:unstable
+      configMap:
+        namespace: db
diff --git a/e2e/testdata/fn-render/format-on-success/db/resources.yaml b/e2e/testdata/fn-render/format-on-success/db/resources.yaml
index e405a520..ac1fd966 100644
--- a/e2e/testdata/fn-render/format-on-success/db/resources.yaml
+++ b/e2e/testdata/fn-render/format-on-success/db/resources.yaml
@@ -16,4 +16,4 @@ kind: StatefulSet
 metadata:
   name: db
 spec:
-    replicas: 3
+  replicas: 3
diff --git a/e2e/testdata/fn-render/format-on-success/resources.yaml b/e2e/testdata/fn-render/format-on-success/resources.yaml
index 71cb6e1a..317f4226 100644
--- a/e2e/testdata/fn-render/format-on-success/resources.yaml
+++ b/e2e/testdata/fn-render/format-on-success/resources.yaml
@@ -18,8 +18,8 @@ metadata:
 spec:
   replicas: 3
   selector:
-      matchLabels:
-        app: nginx
+    matchLabels:
+      app: nginx
   template:
     metadata:
       labels:
@@ -27,6 +27,6 @@ spec:
     spec:
       containers:
         - name: nginx
+          image: nginx:1.14.2
           ports:
             - containerPort: 80
-          image: nginx:1.14.2
diff --git a/e2e/testdata/fn-render/path-index-ancestor/.expected/diff.patch b/e2e/testdata/fn-render/generator-absolute-path/.expected/diff.patch
similarity index 69%
rename from e2e/testdata/fn-render/path-index-ancestor/.expected/diff.patch
rename to e2e/testdata/fn-render/generator-absolute-path/.expected/diff.patch
index 10d977a8..18f40825 100644
--- a/e2e/testdata/fn-render/path-index-ancestor/.expected/diff.patch
+++ b/e2e/testdata/fn-render/generator-absolute-path/.expected/diff.patch
@@ -1,8 +1,8 @@
-diff --git a/deployment_httpbin.yaml b/deployment_httpbin.yaml
+diff --git a/another/file/out.yaml b/another/file/out.yaml
 new file mode 100644
 index 0000000..fa83ef1
 --- /dev/null
-+++ b/deployment_httpbin.yaml
++++ b/another/file/out.yaml
 @@ -0,0 +1,13 @@
 +apiVersion: apps/v1
 +kind: Deployment
@@ -16,4 +16,4 @@ index 0000000..fa83ef1
 +        - name: httpbin
 +          image: kennethreitz/httpbin
 +          ports:
-+            - containerPort: 9876
++            - containerPort: 9876
\ No newline at end of file
diff --git a/e2e/testdata/fn-render/generator-absolute-path/.krmignore b/e2e/testdata/fn-render/generator-absolute-path/.krmignore
new file mode 100644
index 00000000..9d7a4007
--- /dev/null
+++ b/e2e/testdata/fn-render/generator-absolute-path/.krmignore
@@ -0,0 +1,1 @@
+.expected
diff --git a/e2e/testdata/fn-render/path-index-current/db/Kptfile b/e2e/testdata/fn-render/generator-absolute-path/Kptfile
similarity index 50%
rename from e2e/testdata/fn-render/path-index-current/db/Kptfile
rename to e2e/testdata/fn-render/generator-absolute-path/Kptfile
index 27ce7e56..30810450 100644
--- a/e2e/testdata/fn-render/path-index-current/db/Kptfile
+++ b/e2e/testdata/fn-render/generator-absolute-path/Kptfile
@@ -4,5 +4,5 @@ metadata:
   name: db
 pipeline:
   mutators:
-  - image: gcr.io/kpt-fn/starlark:unstable
-    configPath: starlark-httpbin-gen.yaml
\ No newline at end of file
+    - image: gcr.io/kpt-fn/starlark:unstable
+      configPath: starlark-httpbin.yaml
diff --git a/e2e/testdata/fn-render/path-index-current/db/resources.yaml b/e2e/testdata/fn-render/generator-absolute-path/resources.yaml
similarity index 97%
rename from e2e/testdata/fn-render/path-index-current/db/resources.yaml
rename to e2e/testdata/fn-render/generator-absolute-path/resources.yaml
index 685f793b..ac1fd966 100644
--- a/e2e/testdata/fn-render/path-index-current/db/resources.yaml
+++ b/e2e/testdata/fn-render/generator-absolute-path/resources.yaml
@@ -16,4 +16,4 @@ kind: StatefulSet
 metadata:
   name: db
 spec:
-  replicas: 3
\ No newline at end of file
+  replicas: 3
diff --git a/e2e/testdata/fn-render/generator-absolute-path/starlark-httpbin.yaml b/e2e/testdata/fn-render/generator-absolute-path/starlark-httpbin.yaml
new file mode 100644
index 00000000..196d240a
--- /dev/null
+++ b/e2e/testdata/fn-render/generator-absolute-path/starlark-httpbin.yaml
@@ -0,0 +1,60 @@
+# 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: fn.kpt.dev/v1alpha1
+kind: StarlarkRun
+metadata:
+  name: httpbin-gen
+source: |-
+  httpbin_deployment = {
+     "apiVersion": "apps/v1",
+     "kind": "Deployment",
+     "metadata": {
+        "name": "httpbin",
+        "annotations": {
+          "config.kubernetes.io/index": 0,
+          # generates resource in parent directory
+          "config.kubernetes.io/path": "/another/file/out.yaml"
+        }
+     },
+     "spec": {
+        "replicas": 4,
+        "template": {
+           "spec": {
+              "containers": [
+                {
+                  "name": "httpbin",
+                  "image": "kennethreitz/httpbin",
+                  "ports": [
+                    {
+                       "containerPort": 9876
+                    }
+                  ]
+                }
+              ]
+           }
+        }
+    }
+  }
+  # filter to return if resource is HTTPBin resource
+  def is_httpbin(r):
+    return r["apiVersion"] == "apps/v1" and r["kind"] == "Deployment" and r["metadata"]["name"] == "httpbin"
+
+  def ensure_httpbin(resources):
+    for r in resources:
+      if is_httpbin(r):
+        return
+
+    resources.append(httpbin_deployment)
+
+  ensure_httpbin(ctx.resource_list["items"])
diff --git a/e2e/testdata/fn-render/generator/Kptfile b/e2e/testdata/fn-render/generator/Kptfile
index 5a6bec31..a912e7e5 100644
--- a/e2e/testdata/fn-render/generator/Kptfile
+++ b/e2e/testdata/fn-render/generator/Kptfile
@@ -4,9 +4,9 @@ metadata:
   name: app-with-generator
 pipeline:
   mutators:
-  - image: gcr.io/kpt-fn/set-namespace:unstable
-    configMap:
-      namespace: staging
-  - image: gcr.io/kpt-fn/set-labels:unstable
-    configMap:
-      tier: db
\ No newline at end of file
+    - image: gcr.io/kpt-fn/set-namespace:unstable
+      configMap:
+        namespace: staging
+    - image: gcr.io/kpt-fn/set-label:unstable
+      configMap:
+        tier: db
diff --git a/e2e/testdata/fn-render/generator/db/Kptfile b/e2e/testdata/fn-render/generator/db/Kptfile
index 4344a4b4..08c4298f 100644
--- a/e2e/testdata/fn-render/generator/db/Kptfile
+++ b/e2e/testdata/fn-render/generator/db/Kptfile
@@ -4,11 +4,11 @@ metadata:
   name: db
 pipeline:
   mutators:
-  - image: gcr.io/kpt-fn/starlark:unstable
-    configPath: starlark-httpbin.yaml
-  - image: gcr.io/kpt-fn/set-namespace:unstable
-    configMap:
-      namespace: db
-  - image: gcr.io/kpt-fn/set-labels:unstable
-    configMap:
-      app: backend
\ No newline at end of file
+    - image: gcr.io/kpt-fn/starlark:unstable
+      configPath: starlark-httpbin.yaml
+    - image: gcr.io/kpt-fn/set-namespace:unstable
+      configMap:
+        namespace: db
+    - image: gcr.io/kpt-fn/set-label:unstable
+      configMap:
+        app: backend
diff --git a/e2e/testdata/fn-render/generator/db/starlark-httpbin.yaml b/e2e/testdata/fn-render/generator/db/starlark-httpbin.yaml
index b6301a7b..5c7eea79 100644
--- a/e2e/testdata/fn-render/generator/db/starlark-httpbin.yaml
+++ b/e2e/testdata/fn-render/generator/db/starlark-httpbin.yaml
@@ -15,7 +15,7 @@ apiVersion: fn.kpt.dev/v1alpha1
 kind: StarlarkRun
 metadata:
   name: httpbin-gen
-source: |
+source: |-
   httpbin_deployment = {
      "apiVersion": "apps/v1",
      "kind": "Deployment",
@@ -52,4 +52,4 @@ source: |
 
     resources.append(httpbin_deployment)
 
-  ensure_httpbin(ctx.resource_list["items"])
\ No newline at end of file
+  ensure_httpbin(ctx.resource_list["items"])
diff --git a/e2e/testdata/fn-render/inline-fnconfig/Kptfile b/e2e/testdata/fn-render/inline-fnconfig/Kptfile
index e0eb5e2f..0f9559d2 100644
--- a/e2e/testdata/fn-render/inline-fnconfig/Kptfile
+++ b/e2e/testdata/fn-render/inline-fnconfig/Kptfile
@@ -4,12 +4,12 @@ metadata:
   name: app
 pipeline:
   mutators:
-  - image: gcr.io/kpt-fn/set-namespace:unstable
-    configMap:
-      namespace: staging
-  - image: gcr.io/kpt-fn/set-labels:unstable
-    config: # inline config
-      apiVersion: v1
-      kind: ConfigMap
-      data:
-        tier: backend
\ No newline at end of file
+    - image: gcr.io/kpt-fn/set-namespace:unstable
+      configMap:
+        namespace: staging
+    - image: gcr.io/kpt-fn/set-label:unstable
+      config:
+        apiVersion: v1
+        kind: ConfigMap
+        data:
+          tier: backend
diff --git a/e2e/testdata/fn-render/invalid-configmap-fnconfig/Kptfile b/e2e/testdata/fn-render/invalid-configmap-fnconfig/Kptfile
index 05626256..90d45542 100644
--- a/e2e/testdata/fn-render/invalid-configmap-fnconfig/Kptfile
+++ b/e2e/testdata/fn-render/invalid-configmap-fnconfig/Kptfile
@@ -4,13 +4,13 @@ metadata:
   name: app
 pipeline:
   mutators:
-  - image: gcr.io/kpt-fn/set-namespace:unstable
-    configMap:
-      namespace: staging
-  - image: gcr.io/kpt-fn/set-labels:unstable
-    configMap: # invalid config
-      tier: backend
-      envs: # lists are not allowed in configmap
-       - dev
-       - staging
-       - production
\ No newline at end of file
+    - image: gcr.io/kpt-fn/set-namespace:unstable
+      configMap:
+        namespace: staging
+    - image: gcr.io/kpt-fn/set-label:unstable
+      configMap: # invalid config
+        tier: backend
+        envs: # lists are not allowed in configmap
+         - dev
+         - staging
+         - production
diff --git a/e2e/testdata/fn-render/invalid-configmap-fnconfig/resources.yaml b/e2e/testdata/fn-render/invalid-configmap-fnconfig/resources.yaml
index 10cd18bf..7a494c9c 100644
--- a/e2e/testdata/fn-render/invalid-configmap-fnconfig/resources.yaml
+++ b/e2e/testdata/fn-render/invalid-configmap-fnconfig/resources.yaml
@@ -11,7 +11,6 @@
 # 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:
diff --git a/e2e/testdata/fn-render/invalid-inline-fnconfig/.expected/config.yaml b/e2e/testdata/fn-render/invalid-inline-fnconfig/.expected/config.yaml
index baaa6ab5..89dcbcda 100644
--- a/e2e/testdata/fn-render/invalid-inline-fnconfig/.expected/config.yaml
+++ b/e2e/testdata/fn-render/invalid-inline-fnconfig/.expected/config.yaml
@@ -13,4 +13,4 @@
 # limitations under the License.
 
 exitCode: 1
-stdErr: 'Error: invalid pipeline: function "gcr.io/kpt-fn/set-labels:unstable": functionConfig must be a valid KRM resource with `apiVersion` and `kind` fields'
+stdErr: "functionConfig must be a valid KRM resource with `apiVersion` and `kind` fields"
diff --git a/e2e/testdata/fn-render/invalid-inline-fnconfig/Kptfile b/e2e/testdata/fn-render/invalid-inline-fnconfig/Kptfile
index c5aee763..83b7eba8 100644
--- a/e2e/testdata/fn-render/invalid-inline-fnconfig/Kptfile
+++ b/e2e/testdata/fn-render/invalid-inline-fnconfig/Kptfile
@@ -4,13 +4,13 @@ metadata:
   name: app
 pipeline:
   mutators:
-  - image: gcr.io/kpt-fn/set-namespace:unstable
-    configMap:
-      namespace: staging
-  - image: gcr.io/kpt-fn/set-labels:unstable
-    config: # invalid inline config, missing apiVersion/kind
-      tier: backend
-      envs:
-        - dev
-        - staging
-        - production
\ No newline at end of file
+    - image: gcr.io/kpt-fn/set-namespace:unstable
+      configMap:
+        namespace: staging
+    - image: gcr.io/kpt-fn/set-label:unstable
+      config: # invalid inline config, missing apiVersion/kind
+        envs:
+          - dev
+          - staging
+          - production
+        tier: backend
diff --git a/e2e/testdata/fn-render/invalid-inline-fnconfig/resources.yaml b/e2e/testdata/fn-render/invalid-inline-fnconfig/resources.yaml
index 10cd18bf..7a494c9c 100644
--- a/e2e/testdata/fn-render/invalid-inline-fnconfig/resources.yaml
+++ b/e2e/testdata/fn-render/invalid-inline-fnconfig/resources.yaml
@@ -11,7 +11,6 @@
 # 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:
diff --git a/e2e/testdata/fn-render/invalid-kptfile/resources.yaml b/e2e/testdata/fn-render/invalid-kptfile/resources.yaml
index 10cd18bf..7a494c9c 100644
--- a/e2e/testdata/fn-render/invalid-kptfile/resources.yaml
+++ b/e2e/testdata/fn-render/invalid-kptfile/resources.yaml
@@ -11,7 +11,6 @@
 # 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:
diff --git a/e2e/testdata/fn-render/krmignore/Kptfile b/e2e/testdata/fn-render/krmignore/Kptfile
index 05263042..1a329d3d 100644
--- a/e2e/testdata/fn-render/krmignore/Kptfile
+++ b/e2e/testdata/fn-render/krmignore/Kptfile
@@ -4,6 +4,6 @@ metadata:
   name: app
 pipeline:
   mutators:
-  - image: gcr.io/kpt-fn/set-namespace:unstable
-    configMap:
-      namespace: staging
\ No newline at end of file
+    - image: gcr.io/kpt-fn/set-namespace:unstable
+      configMap:
+        namespace: staging
diff --git a/e2e/testdata/fn-render/krmignore/resources.yaml b/e2e/testdata/fn-render/krmignore/resources.yaml
index e4079d23..2e604e49 100644
--- a/e2e/testdata/fn-render/krmignore/resources.yaml
+++ b/e2e/testdata/fn-render/krmignore/resources.yaml
@@ -11,7 +11,6 @@
 # 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:
diff --git a/e2e/testdata/fn-render/kubeval-failure/Kptfile b/e2e/testdata/fn-render/kubeval-failure/Kptfile
index 8d92eb89..62970d95 100644
--- a/e2e/testdata/fn-render/kubeval-failure/Kptfile
+++ b/e2e/testdata/fn-render/kubeval-failure/Kptfile
@@ -4,6 +4,6 @@ metadata:
   name: app
 pipeline:
   validators:
-  - image: gcr.io/kpt-fn/kubeval:unstable
-    configMap:
-      strict: "true"
+    - image: gcr.io/kpt-fn/kubeval:unstable
+      configMap:
+        strict: "true"
diff --git a/e2e/testdata/fn-render/kubeval-failure/resources.yaml b/e2e/testdata/fn-render/kubeval-failure/resources.yaml
index 9ccaaa45..085379f3 100644
--- a/e2e/testdata/fn-render/kubeval-failure/resources.yaml
+++ b/e2e/testdata/fn-render/kubeval-failure/resources.yaml
@@ -11,7 +11,6 @@
 # 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:
diff --git a/e2e/testdata/fn-render/missing-fn-image/.expected/config.yaml b/e2e/testdata/fn-render/missing-fn-image/.expected/config.yaml
index 20d2b71d..fc5ed75b 100644
--- a/e2e/testdata/fn-render/missing-fn-image/.expected/config.yaml
+++ b/e2e/testdata/fn-render/missing-fn-image/.expected/config.yaml
@@ -13,7 +13,7 @@
 # limitations under the License.
 
 exitCode: 1
-stdErr: "Error: function image \"gcr.io/kpt-fn/set-labels1\" doesn't exist"
+stdErr: 'Error: Function image "gcr.io/kpt-fn/dne" doesn''t exist'
 stdOut: |
   [RUNNING] "gcr.io/kpt-fn/set-namespace:unstable"
   [PASS] "gcr.io/kpt-fn/set-namespace:unstable"
diff --git a/e2e/testdata/fn-render/missing-fn-image/Kptfile b/e2e/testdata/fn-render/missing-fn-image/Kptfile
index 7cc1617f..fdda766d 100644
--- a/e2e/testdata/fn-render/missing-fn-image/Kptfile
+++ b/e2e/testdata/fn-render/missing-fn-image/Kptfile
@@ -4,9 +4,9 @@ metadata:
   name: app
 pipeline:
   mutators:
-  - image: gcr.io/kpt-fn/set-namespace:unstable
-    configMap:
-      namespace: staging
-  - image: gcr.io/kpt-fn/set-labels1 # non-existing image
-    configMap:
-      tier: backend
\ No newline at end of file
+    - image: gcr.io/kpt-fn/set-namespace:unstable
+      configMap:
+        namespace: staging
+    - image: gcr.io/kpt-fn/dne # non-existing image
+      configMap:
+        tier: backend
diff --git a/e2e/testdata/fn-render/missing-fn-image/resources.yaml b/e2e/testdata/fn-render/missing-fn-image/resources.yaml
index 10cd18bf..7a494c9c 100644
--- a/e2e/testdata/fn-render/missing-fn-image/resources.yaml
+++ b/e2e/testdata/fn-render/missing-fn-image/resources.yaml
@@ -11,7 +11,6 @@
 # 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:
diff --git a/e2e/testdata/fn-render/missing-fnconfig/Kptfile b/e2e/testdata/fn-render/missing-fnconfig/Kptfile
index eadca3f0..332ab313 100644
--- a/e2e/testdata/fn-render/missing-fnconfig/Kptfile
+++ b/e2e/testdata/fn-render/missing-fnconfig/Kptfile
@@ -4,8 +4,8 @@ metadata:
   name: app-with-db
 pipeline:
   mutators:
-  - image: gcr.io/kpt-fn/set-namespace:unstable
-    configMap:
-      namespace: staging
-  - image: gcr.io/kpt-fn/set-labels:unstable
-    configPath: labelconfig1.yaml
+    - image: gcr.io/kpt-fn/set-namespace:unstable
+      configMap:
+        namespace: staging
+    - image: gcr.io/kpt-fn/set-labels:unstable
+      configPath: labelconfig1.yaml
\ No newline at end of file
diff --git a/e2e/testdata/fn-render/missing-fnconfig/db/Kptfile b/e2e/testdata/fn-render/missing-fnconfig/db/Kptfile
index 0396b671..7e8e9811 100644
--- a/e2e/testdata/fn-render/missing-fnconfig/db/Kptfile
+++ b/e2e/testdata/fn-render/missing-fnconfig/db/Kptfile
@@ -4,9 +4,9 @@ metadata:
   name: db
 pipeline:
   mutators:
-  - image: gcr.io/kpt-fn/set-namespace:unstable
-    configMap:
-      namespace: db
-  - image: gcr.io/kpt-fn/set-labels:unstable
-    configMap:
-      app: backend
\ No newline at end of file
+    - image: gcr.io/kpt-fn/set-namespace:unstable
+      configMap:
+        namespace: db
+    - image: gcr.io/kpt-fn/set-labels:unstable
+      configMap:
+        app: backend
diff --git a/e2e/testdata/fn-render/missing-fnconfig/db/resources.yaml b/e2e/testdata/fn-render/missing-fnconfig/db/resources.yaml
index c88ef2de..ac1fd966 100644
--- a/e2e/testdata/fn-render/missing-fnconfig/db/resources.yaml
+++ b/e2e/testdata/fn-render/missing-fnconfig/db/resources.yaml
@@ -11,7 +11,6 @@
 # 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:
diff --git a/e2e/testdata/fn-render/missing-fnconfig/labelconfig.yaml b/e2e/testdata/fn-render/missing-fnconfig/labelconfig.yaml
index a007f605..aabf4956 100644
--- a/e2e/testdata/fn-render/missing-fnconfig/labelconfig.yaml
+++ b/e2e/testdata/fn-render/missing-fnconfig/labelconfig.yaml
@@ -11,10 +11,9 @@
 # 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
\ No newline at end of file
+  tier: db
diff --git a/e2e/testdata/fn-render/missing-fnconfig/resources.yaml b/e2e/testdata/fn-render/missing-fnconfig/resources.yaml
index 10cd18bf..7a494c9c 100644
--- a/e2e/testdata/fn-render/missing-fnconfig/resources.yaml
+++ b/e2e/testdata/fn-render/missing-fnconfig/resources.yaml
@@ -11,7 +11,6 @@
 # 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:
diff --git a/e2e/testdata/fn-render/missing-kptfile/resources.yaml b/e2e/testdata/fn-render/missing-kptfile/resources.yaml
index 10cd18bf..7a494c9c 100644
--- a/e2e/testdata/fn-render/missing-kptfile/resources.yaml
+++ b/e2e/testdata/fn-render/missing-kptfile/resources.yaml
@@ -11,7 +11,6 @@
 # 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:
diff --git a/e2e/testdata/fn-render/multiple-fnconfig/.expected/config.yaml b/e2e/testdata/fn-render/multiple-fnconfig/.expected/config.yaml
index 5f3e6ce0..188ee542 100644
--- a/e2e/testdata/fn-render/multiple-fnconfig/.expected/config.yaml
+++ b/e2e/testdata/fn-render/multiple-fnconfig/.expected/config.yaml
@@ -13,4 +13,4 @@
 # limitations under the License.
 
 exitCode: 1
-stdErr: "following fields are mutually exclusive: 'config', 'configMap', 'configPath'. Got \"configPath, configMap\""
+stdErr: 'only one of ''config'', ''configMap'', ''configPath'' can be specified. Got "configPath, configMap"'
diff --git a/e2e/testdata/fn-render/multiple-fnconfig/Kptfile b/e2e/testdata/fn-render/multiple-fnconfig/Kptfile
index 690cd892..14c5fa6d 100644
--- a/e2e/testdata/fn-render/multiple-fnconfig/Kptfile
+++ b/e2e/testdata/fn-render/multiple-fnconfig/Kptfile
@@ -4,7 +4,7 @@ metadata:
   name: app
 pipeline:
   mutators:
-  - image: gcr.io/kpt-fn/set-namespace:unstable
-    configPath: configmap.yaml
-    configMap:
-      namespace: staging
+    - image: gcr.io/kpt-fn/set-namespace:unstable
+      configMap:
+        namespace: staging
+      configPath: configmap.yaml
diff --git a/e2e/testdata/fn-render/multiple-fnconfig/configmap.yaml b/e2e/testdata/fn-render/multiple-fnconfig/configmap.yaml
index c0bad6da..6f5cbfc5 100644
--- a/e2e/testdata/fn-render/multiple-fnconfig/configmap.yaml
+++ b/e2e/testdata/fn-render/multiple-fnconfig/configmap.yaml
@@ -11,7 +11,6 @@
 # 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:
diff --git a/e2e/testdata/fn-render/no-fnconfig/Kptfile b/e2e/testdata/fn-render/no-fnconfig/Kptfile
index 2c076d3b..5be6187e 100644
--- a/e2e/testdata/fn-render/no-fnconfig/Kptfile
+++ b/e2e/testdata/fn-render/no-fnconfig/Kptfile
@@ -4,7 +4,7 @@ metadata:
   name: app
 pipeline:
   mutators:
-  - image: gcr.io/kpt-fn/set-namespace:unstable
-    configMap:
-      namespace: staging
-  - image: gcr.io/kpt-fn/set-labels:unstable
\ No newline at end of file
+    - image: gcr.io/kpt-fn/set-namespace:unstable
+      configMap:
+        namespace: staging
+    - image: gcr.io/kpt-fn/set-labels:unstable
\ No newline at end of file
diff --git a/e2e/testdata/fn-render/no-fnconfig/resources.yaml b/e2e/testdata/fn-render/no-fnconfig/resources.yaml
index 10cd18bf..7a494c9c 100644
--- a/e2e/testdata/fn-render/no-fnconfig/resources.yaml
+++ b/e2e/testdata/fn-render/no-fnconfig/resources.yaml
@@ -11,7 +11,6 @@
 # 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:
diff --git a/e2e/testdata/fn-render/no-format-on-failure/Kptfile b/e2e/testdata/fn-render/no-format-on-failure/Kptfile
index 364176ad..00ef5b2b 100644
--- a/e2e/testdata/fn-render/no-format-on-failure/Kptfile
+++ b/e2e/testdata/fn-render/no-format-on-failure/Kptfile
@@ -4,7 +4,7 @@ metadata:
   name: app-with-db
 pipeline:
   mutators:
-  - image: gcr.io/kpt-fn/set-namespace:unstable
-    configMap:
-      namespace:
-        hello: world
+    - image: gcr.io/kpt-fn/set-namespace:unstable
+      configMap:
+        namespace:
+          hello: world
diff --git a/e2e/testdata/fn-render/no-format-on-failure/db/Kptfile b/e2e/testdata/fn-render/no-format-on-failure/db/Kptfile
index 5f58d54f..cb0de4ab 100644
--- a/e2e/testdata/fn-render/no-format-on-failure/db/Kptfile
+++ b/e2e/testdata/fn-render/no-format-on-failure/db/Kptfile
@@ -4,6 +4,6 @@ metadata:
   name: db
 pipeline:
   mutators:
-  - image: gcr.io/kpt-fn/set-namespace:unstable
-    configMap:
-      namespace: db
+    - image: gcr.io/kpt-fn/set-namespace:unstable
+      configMap:
+        namespace: db
diff --git a/e2e/testdata/fn-render/no-format-on-failure/db/resources.yaml b/e2e/testdata/fn-render/no-format-on-failure/db/resources.yaml
index e405a520..ac1fd966 100644
--- a/e2e/testdata/fn-render/no-format-on-failure/db/resources.yaml
+++ b/e2e/testdata/fn-render/no-format-on-failure/db/resources.yaml
@@ -16,4 +16,4 @@ kind: StatefulSet
 metadata:
   name: db
 spec:
-    replicas: 3
+  replicas: 3
diff --git a/e2e/testdata/fn-render/no-format-on-failure/resources.yaml b/e2e/testdata/fn-render/no-format-on-failure/resources.yaml
index 085e796a..7a494c9c 100644
--- a/e2e/testdata/fn-render/no-format-on-failure/resources.yaml
+++ b/e2e/testdata/fn-render/no-format-on-failure/resources.yaml
@@ -16,7 +16,7 @@ kind: Deployment
 metadata:
   name: nginx-deployment
 spec:
-    replicas: 3
+  replicas: 3
 ---
 apiVersion: custom.io/v1
 kind: Custom
diff --git a/e2e/testdata/fn-render/no-pipeline-in-subpackage/.expected/diff.patch b/e2e/testdata/fn-render/no-pipeline-in-subpackage/.expected/diff.patch
index 9e751849..4e76454a 100644
--- a/e2e/testdata/fn-render/no-pipeline-in-subpackage/.expected/diff.patch
+++ b/e2e/testdata/fn-render/no-pipeline-in-subpackage/.expected/diff.patch
@@ -1,13 +1,8 @@
 diff --git a/db/resources.yaml b/db/resources.yaml
-index 10cd18b..a9dd224 100644
+index 7a494c9..a9dd224 100644
 --- a/db/resources.yaml
 +++ b/db/resources.yaml
-@@ -11,17 +11,29 @@
- # 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
+@@ -15,12 +15,25 @@ apiVersion: apps/v1
  kind: Deployment
  metadata:
    name: nginx-deployment
diff --git a/e2e/testdata/fn-render/no-pipeline-in-subpackage/Kptfile b/e2e/testdata/fn-render/no-pipeline-in-subpackage/Kptfile
index 2b15e8f9..1a5dd33c 100644
--- a/e2e/testdata/fn-render/no-pipeline-in-subpackage/Kptfile
+++ b/e2e/testdata/fn-render/no-pipeline-in-subpackage/Kptfile
@@ -4,9 +4,9 @@ metadata:
   name: app
 pipeline:
   mutators:
-  - image: gcr.io/kpt-fn/set-namespace:unstable
-    configMap:
-      namespace: staging
-  - image: gcr.io/kpt-fn/set-labels:unstable
-    configMap:
-      tier: backend
\ No newline at end of file
+    - image: gcr.io/kpt-fn/set-namespace:unstable
+      configMap:
+        namespace: staging
+    - image: gcr.io/kpt-fn/set-label:unstable
+      configMap:
+        tier: backend
diff --git a/e2e/testdata/fn-render/no-pipeline-in-subpackage/db/Kptfile b/e2e/testdata/fn-render/no-pipeline-in-subpackage/db/Kptfile
index e30e1348..883da19e 100644
--- a/e2e/testdata/fn-render/no-pipeline-in-subpackage/db/Kptfile
+++ b/e2e/testdata/fn-render/no-pipeline-in-subpackage/db/Kptfile
@@ -1,4 +1,4 @@
 apiVersion: kpt.dev/v1alpha2
 kind: Kptfile
 metadata:
-  name: app
\ No newline at end of file
+  name: app
diff --git a/e2e/testdata/fn-render/no-pipeline-in-subpackage/db/resources.yaml b/e2e/testdata/fn-render/no-pipeline-in-subpackage/db/resources.yaml
index 10cd18bf..7a494c9c 100644
--- a/e2e/testdata/fn-render/no-pipeline-in-subpackage/db/resources.yaml
+++ b/e2e/testdata/fn-render/no-pipeline-in-subpackage/db/resources.yaml
@@ -11,7 +11,6 @@
 # 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:
diff --git a/e2e/testdata/fn-render/no-pipeline/Kptfile b/e2e/testdata/fn-render/no-pipeline/Kptfile
index e30e1348..883da19e 100644
--- a/e2e/testdata/fn-render/no-pipeline/Kptfile
+++ b/e2e/testdata/fn-render/no-pipeline/Kptfile
@@ -1,4 +1,4 @@
 apiVersion: kpt.dev/v1alpha2
 kind: Kptfile
 metadata:
-  name: app
\ No newline at end of file
+  name: app
diff --git a/e2e/testdata/fn-render/no-pipeline/resources.yaml b/e2e/testdata/fn-render/no-pipeline/resources.yaml
index 10cd18bf..7a494c9c 100644
--- a/e2e/testdata/fn-render/no-pipeline/resources.yaml
+++ b/e2e/testdata/fn-render/no-pipeline/resources.yaml
@@ -11,7 +11,6 @@
 # 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:
diff --git a/e2e/testdata/fn-render/path-index-ancestor/.expected/config.yaml b/e2e/testdata/fn-render/path-index-ancestor/.expected/config.yaml
index bd487dc9..77383b91 100644
--- a/e2e/testdata/fn-render/path-index-ancestor/.expected/config.yaml
+++ b/e2e/testdata/fn-render/path-index-ancestor/.expected/config.yaml
@@ -12,8 +12,5 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# This package is nonIdempotent because functions in the subpackage (db)
-# do not have knowledge of the resources in the parent package. Therefore
-# when it runs twice, it tries to recreate the same resource in the parent,
-# resulting in a filepath conflict and throwing an error.
-nonIdempotent: true
\ No newline at end of file
+exitCode: 1
+stdErr: "Error: function must not modify resources outside of package: resource has path ../deployment_httpbin.yaml"
\ No newline at end of file
diff --git a/e2e/testdata/fn-render/path-index-ancestor/Kptfile b/e2e/testdata/fn-render/path-index-ancestor/Kptfile
index 01d5e0e5..7b3bc56d 100644
--- a/e2e/testdata/fn-render/path-index-ancestor/Kptfile
+++ b/e2e/testdata/fn-render/path-index-ancestor/Kptfile
@@ -1,4 +1,4 @@
 apiVersion: kpt.dev/v1alpha2
 kind: Kptfile
 metadata:
-  name: app-with-generator
\ No newline at end of file
+  name: app-with-generator
diff --git a/e2e/testdata/fn-render/path-index-ancestor/db/Kptfile b/e2e/testdata/fn-render/path-index-ancestor/db/Kptfile
index 27ce7e56..142ea1fd 100644
--- a/e2e/testdata/fn-render/path-index-ancestor/db/Kptfile
+++ b/e2e/testdata/fn-render/path-index-ancestor/db/Kptfile
@@ -4,5 +4,5 @@ metadata:
   name: db
 pipeline:
   mutators:
-  - image: gcr.io/kpt-fn/starlark:unstable
-    configPath: starlark-httpbin-gen.yaml
\ No newline at end of file
+    - image: gcr.io/kpt-fn/starlark:unstable
+      configPath: starlark-httpbin-gen.yaml
diff --git a/e2e/testdata/fn-render/path-index-ancestor/db/starlark-httpbin-gen.yaml b/e2e/testdata/fn-render/path-index-ancestor/db/starlark-httpbin-gen.yaml
index e6dfd4f7..fb96fa87 100644
--- a/e2e/testdata/fn-render/path-index-ancestor/db/starlark-httpbin-gen.yaml
+++ b/e2e/testdata/fn-render/path-index-ancestor/db/starlark-httpbin-gen.yaml
@@ -11,12 +11,11 @@
 # 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: fn.kpt.dev/v1alpha1
 kind: StarlarkRun
 metadata:
   name: httpbin-gen
-source: |
+source: |-
   httpbin_deployment = {
      "apiVersion": "apps/v1",
      "kind": "Deployment",
@@ -57,4 +56,4 @@ source: |
         return
     resources.append(httpbin_deployment)
 
-  ensure_httpbin(ctx.resource_list["items"])
\ No newline at end of file
+  ensure_httpbin(ctx.resource_list["items"])
diff --git a/e2e/testdata/fn-render/path-index-current/.expected/diff.patch b/e2e/testdata/fn-render/path-index-current/.expected/diff.patch
index fe08256d..2b504552 100644
--- a/e2e/testdata/fn-render/path-index-current/.expected/diff.patch
+++ b/e2e/testdata/fn-render/path-index-current/.expected/diff.patch
@@ -1,8 +1,8 @@
-diff --git a/db/deployment_httpbin.yaml b/db/deployment_httpbin.yaml
+diff --git a/deployment_httpbin.yaml b/deployment_httpbin.yaml
 new file mode 100644
 index 0000000..fa83ef1
 --- /dev/null
-+++ b/db/deployment_httpbin.yaml
++++ b/deployment_httpbin.yaml
 @@ -0,0 +1,13 @@
 +apiVersion: apps/v1
 +kind: Deployment
@@ -16,15 +16,4 @@ index 0000000..fa83ef1
 +        - name: httpbin
 +          image: kennethreitz/httpbin
 +          ports:
-+            - containerPort: 9876
-diff --git a/db/resources.yaml b/db/resources.yaml
-index 685f793..ac1fd96 100644
---- a/db/resources.yaml
-+++ b/db/resources.yaml
-@@ -16,4 +16,4 @@ kind: StatefulSet
- metadata:
-   name: db
- spec:
--  replicas: 3
-\ No newline at end of file
-+  replicas: 3
++            - containerPort: 9876
\ No newline at end of file
diff --git a/e2e/testdata/fn-render/path-index-current/Kptfile b/e2e/testdata/fn-render/path-index-current/Kptfile
index 01d5e0e5..375117ba 100644
--- a/e2e/testdata/fn-render/path-index-current/Kptfile
+++ b/e2e/testdata/fn-render/path-index-current/Kptfile
@@ -1,4 +1,8 @@
 apiVersion: kpt.dev/v1alpha2
 kind: Kptfile
 metadata:
-  name: app-with-generator
\ No newline at end of file
+  name: app-with-generator
+pipeline:
+  mutators:
+    - image: gcr.io/kpt-fn/starlark:unstable
+      configPath: starlark-httpbin-gen.yaml
diff --git a/e2e/testdata/fn-render/path-index-current/db/starlark-httpbin-gen.yaml b/e2e/testdata/fn-render/path-index-current/starlark-httpbin-gen.yaml
similarity index 96%
rename from e2e/testdata/fn-render/path-index-current/db/starlark-httpbin-gen.yaml
rename to e2e/testdata/fn-render/path-index-current/starlark-httpbin-gen.yaml
index 3eec60db..43996f16 100644
--- a/e2e/testdata/fn-render/path-index-current/db/starlark-httpbin-gen.yaml
+++ b/e2e/testdata/fn-render/path-index-current/starlark-httpbin-gen.yaml
@@ -11,12 +11,11 @@
 # 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: fn.kpt.dev/v1alpha1
 kind: StarlarkRun
 metadata:
   name: httpbin-gen
-source: |
+source: |-
   httpbin_deployment = {
      "apiVersion": "apps/v1",
      "kind": "Deployment",
@@ -58,4 +57,4 @@ source: |
 
     resources.append(httpbin_deployment)
 
-  ensure_httpbin(ctx.resource_list["items"])
\ No newline at end of file
+  ensure_httpbin(ctx.resource_list["items"])
diff --git a/e2e/testdata/fn-render/path-index-descendent/.expected/diff.patch b/e2e/testdata/fn-render/path-index-descendent/.expected/diff.patch
index fe08256d..6212e438 100644
--- a/e2e/testdata/fn-render/path-index-descendent/.expected/diff.patch
+++ b/e2e/testdata/fn-render/path-index-descendent/.expected/diff.patch
@@ -16,15 +16,4 @@ index 0000000..fa83ef1
 +        - name: httpbin
 +          image: kennethreitz/httpbin
 +          ports:
-+            - containerPort: 9876
-diff --git a/db/resources.yaml b/db/resources.yaml
-index 685f793..ac1fd96 100644
---- a/db/resources.yaml
-+++ b/db/resources.yaml
-@@ -16,4 +16,4 @@ kind: StatefulSet
- metadata:
-   name: db
- spec:
--  replicas: 3
-\ No newline at end of file
-+  replicas: 3
++            - containerPort: 9876
\ No newline at end of file
diff --git a/e2e/testdata/fn-render/path-index-descendent/Kptfile b/e2e/testdata/fn-render/path-index-descendent/Kptfile
index 748fae59..375117ba 100644
--- a/e2e/testdata/fn-render/path-index-descendent/Kptfile
+++ b/e2e/testdata/fn-render/path-index-descendent/Kptfile
@@ -4,5 +4,5 @@ metadata:
   name: app-with-generator
 pipeline:
   mutators:
-  - image: gcr.io/kpt-fn/starlark:unstable
-    configPath: starlark-httpbin-gen.yaml
+    - image: gcr.io/kpt-fn/starlark:unstable
+      configPath: starlark-httpbin-gen.yaml
diff --git a/e2e/testdata/fn-render/path-index-descendent/db/Kptfile b/e2e/testdata/fn-render/path-index-descendent/db/Kptfile
index ed815a86..60f507d0 100644
--- a/e2e/testdata/fn-render/path-index-descendent/db/Kptfile
+++ b/e2e/testdata/fn-render/path-index-descendent/db/Kptfile
@@ -1,4 +1,4 @@
 apiVersion: kpt.dev/v1alpha2
 kind: Kptfile
 metadata:
-  name: db
\ No newline at end of file
+  name: db
diff --git a/e2e/testdata/fn-render/path-index-descendent/db/resources.yaml b/e2e/testdata/fn-render/path-index-descendent/db/resources.yaml
index 685f793b..ac1fd966 100644
--- a/e2e/testdata/fn-render/path-index-descendent/db/resources.yaml
+++ b/e2e/testdata/fn-render/path-index-descendent/db/resources.yaml
@@ -16,4 +16,4 @@ kind: StatefulSet
 metadata:
   name: db
 spec:
-  replicas: 3
\ No newline at end of file
+  replicas: 3
diff --git a/e2e/testdata/fn-render/path-index-descendent/starlark-httpbin-gen.yaml b/e2e/testdata/fn-render/path-index-descendent/starlark-httpbin-gen.yaml
index d30dc89c..0e4efa6e 100644
--- a/e2e/testdata/fn-render/path-index-descendent/starlark-httpbin-gen.yaml
+++ b/e2e/testdata/fn-render/path-index-descendent/starlark-httpbin-gen.yaml
@@ -11,12 +11,11 @@
 # 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: fn.kpt.dev/v1alpha1
 kind: StarlarkRun
 metadata:
   name: httpbin-gen
-source: |
+source: |-
   httpbin_deployment = {
      "apiVersion": "apps/v1",
      "kind": "Deployment",
@@ -58,4 +57,4 @@ source: |
 
     resources.append(httpbin_deployment)
 
-  ensure_httpbin(ctx.resource_list["items"])
\ No newline at end of file
+  ensure_httpbin(ctx.resource_list["items"])
diff --git a/e2e/testdata/fn-render/path-index-duplicate/.expected/config.yaml b/e2e/testdata/fn-render/path-index-duplicate/.expected/config.yaml
index 08c0556e..78c04966 100644
--- a/e2e/testdata/fn-render/path-index-duplicate/.expected/config.yaml
+++ b/e2e/testdata/fn-render/path-index-duplicate/.expected/config.yaml
@@ -18,4 +18,4 @@ exitCode: 1
 stdErr: 'resource at path "resources.yaml" and index "0" already exists'
 stdOut: |
   [RUNNING] "gcr.io/kpt-fn/starlark:unstable"
-  [PASS] "gcr.io/kpt-fn/starlark:unstable"
+  [FAIL] "gcr.io/kpt-fn/starlark:unstable"
diff --git a/e2e/testdata/fn-render/path-index-duplicate/Kptfile b/e2e/testdata/fn-render/path-index-duplicate/Kptfile
index e5061302..fbc2ca19 100644
--- a/e2e/testdata/fn-render/path-index-duplicate/Kptfile
+++ b/e2e/testdata/fn-render/path-index-duplicate/Kptfile
@@ -4,5 +4,5 @@ metadata:
   name: db
 pipeline:
   mutators:
-  - image: gcr.io/kpt-fn/starlark:unstable
-    configPath: starlark-gen-duplicate-path.yaml
\ No newline at end of file
+    - image: gcr.io/kpt-fn/starlark:unstable
+      configPath: starlark-gen-duplicate-path.yaml
diff --git a/e2e/testdata/fn-render/path-index-duplicate/resources.yaml b/e2e/testdata/fn-render/path-index-duplicate/resources.yaml
index 3403ac2b..2bc0f267 100644
--- a/e2e/testdata/fn-render/path-index-duplicate/resources.yaml
+++ b/e2e/testdata/fn-render/path-index-duplicate/resources.yaml
@@ -11,7 +11,6 @@
 # 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:
diff --git a/e2e/testdata/fn-render/path-index-duplicate/starlark-gen-duplicate-path.yaml b/e2e/testdata/fn-render/path-index-duplicate/starlark-gen-duplicate-path.yaml
index 7199eebf..0cde54fa 100644
--- a/e2e/testdata/fn-render/path-index-duplicate/starlark-gen-duplicate-path.yaml
+++ b/e2e/testdata/fn-render/path-index-duplicate/starlark-gen-duplicate-path.yaml
@@ -11,12 +11,11 @@
 # 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: fn.kpt.dev/v1alpha1
 kind: StarlarkRun
 metadata:
   name: duplicate-path-generator
-source: |
+source: |-
   httpbin_deployment = {
      "apiVersion": "apps/v1",
      "kind": "Deployment",
@@ -58,4 +57,4 @@ source: |
 
     resources.append(httpbin_deployment)
 
-  ensure_httpbin(ctx.resource_list["items"])
\ No newline at end of file
+  ensure_httpbin(ctx.resource_list["items"])
diff --git a/e2e/testdata/fn-render/path-index-outofpackage/.expected/config.yaml b/e2e/testdata/fn-render/path-index-outofpackage/.expected/config.yaml
index 54fb5f42..a2e30ce7 100644
--- a/e2e/testdata/fn-render/path-index-outofpackage/.expected/config.yaml
+++ b/e2e/testdata/fn-render/path-index-outofpackage/.expected/config.yaml
@@ -12,8 +12,5 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# This package is nonIdempotent because functions in the subpackage (db)
-# do not have knowledge of the resources outside of this package. Therefore
-# when it runs twice, it tries to recreate the same resource in the directory,
-# resulting in a filepath conflict and throwing an error.
-nonIdempotent: true
+exitCode: 1
+stdErr: "Error: function must not modify resources outside of package: resource has path ../notpkg/deployment_httpbin.yaml"
\ No newline at end of file
diff --git a/e2e/testdata/fn-render/path-index-outofpackage/.expected/diff.patch b/e2e/testdata/fn-render/path-index-outofpackage/.expected/diff.patch
deleted file mode 100644
index be184eb6..00000000
--- a/e2e/testdata/fn-render/path-index-outofpackage/.expected/diff.patch
+++ /dev/null
@@ -1,19 +0,0 @@
-diff --git a/notpkg/deployment_httpbin.yaml b/notpkg/deployment_httpbin.yaml
-new file mode 100644
-index 0000000..fa83ef1
---- /dev/null
-+++ b/notpkg/deployment_httpbin.yaml
-@@ -0,0 +1,13 @@
-+apiVersion: apps/v1
-+kind: Deployment
-+metadata:
-+  name: httpbin
-+spec:
-+  replicas: 4
-+  template:
-+    spec:
-+      containers:
-+        - name: httpbin
-+          image: kennethreitz/httpbin
-+          ports:
-+            - containerPort: 9876
diff --git a/e2e/testdata/fn-render/path-index-outofpackage/Kptfile b/e2e/testdata/fn-render/path-index-outofpackage/Kptfile
index 01d5e0e5..7b3bc56d 100644
--- a/e2e/testdata/fn-render/path-index-outofpackage/Kptfile
+++ b/e2e/testdata/fn-render/path-index-outofpackage/Kptfile
@@ -1,4 +1,4 @@
 apiVersion: kpt.dev/v1alpha2
 kind: Kptfile
 metadata:
-  name: app-with-generator
\ No newline at end of file
+  name: app-with-generator
diff --git a/e2e/testdata/fn-render/path-index-outofpackage/db/Kptfile b/e2e/testdata/fn-render/path-index-outofpackage/db/Kptfile
index 27ce7e56..142ea1fd 100644
--- a/e2e/testdata/fn-render/path-index-outofpackage/db/Kptfile
+++ b/e2e/testdata/fn-render/path-index-outofpackage/db/Kptfile
@@ -4,5 +4,5 @@ metadata:
   name: db
 pipeline:
   mutators:
-  - image: gcr.io/kpt-fn/starlark:unstable
-    configPath: starlark-httpbin-gen.yaml
\ No newline at end of file
+    - image: gcr.io/kpt-fn/starlark:unstable
+      configPath: starlark-httpbin-gen.yaml
diff --git a/e2e/testdata/fn-render/path-index-outofpackage/db/starlark-httpbin-gen.yaml b/e2e/testdata/fn-render/path-index-outofpackage/db/starlark-httpbin-gen.yaml
index 8a057acf..17f2afbf 100644
--- a/e2e/testdata/fn-render/path-index-outofpackage/db/starlark-httpbin-gen.yaml
+++ b/e2e/testdata/fn-render/path-index-outofpackage/db/starlark-httpbin-gen.yaml
@@ -11,12 +11,11 @@
 # 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: fn.kpt.dev/v1alpha1
 kind: StarlarkRun
 metadata:
   name: httpbin-gen
-source: |
+source: |-
   httpbin_deployment = {
      "apiVersion": "apps/v1",
      "kind": "Deployment",
@@ -57,4 +56,4 @@ source: |
         return
     resources.append(httpbin_deployment)
 
-  ensure_httpbin(ctx.resource_list["items"])
\ No newline at end of file
+  ensure_httpbin(ctx.resource_list["items"])
diff --git a/e2e/testdata/fn-render/resource-deletion/.expected/diff.patch b/e2e/testdata/fn-render/resource-deletion/.expected/diff.patch
index c8214c3a..bb7dd818 100644
--- a/e2e/testdata/fn-render/resource-deletion/.expected/diff.patch
+++ b/e2e/testdata/fn-render/resource-deletion/.expected/diff.patch
@@ -1,9 +1,9 @@
 diff --git a/deployment_httpbin.yaml b/deployment_httpbin.yaml
 deleted file mode 100644
-index 0ef5d14..0000000
+index 44bfc6b..0000000
 --- a/deployment_httpbin.yaml
 +++ /dev/null
-@@ -1,37 +0,0 @@
+@@ -1,36 +0,0 @@
 -# Copyright 2021 Google LLC
 -#
 -# Licensed under the Apache License, Version 2.0 (the "License");
@@ -17,15 +17,14 @@ index 0ef5d14..0000000
 -# 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: httpbin
+-  namespace: staging
 -  labels:
 -    app: backend
 -    tier: db
--  name: httpbin
--  namespace: staging
 -spec:
 -  replicas: 4
 -  selector:
@@ -39,8 +38,8 @@ index 0ef5d14..0000000
 -        tier: db
 -    spec:
 -      containers:
--      - image: kennethreitz/httpbin
--        name: httpbin
+-        - name: httpbin
+-          image: kennethreitz/httpbin
 diff --git a/resources.yaml b/resources.yaml
 index 7a494c9..a9dd224 100644
 --- a/resources.yaml
@@ -70,4 +69,4 @@ index 7a494c9..a9dd224 100644
 +  labels:
 +    tier: backend
  spec:
-   image: nginx:1.2.3
+   image: nginx:1.2.3
\ No newline at end of file
diff --git a/e2e/testdata/fn-render/resource-deletion/Kptfile b/e2e/testdata/fn-render/resource-deletion/Kptfile
index 23a9f778..e45ddb33 100644
--- a/e2e/testdata/fn-render/resource-deletion/Kptfile
+++ b/e2e/testdata/fn-render/resource-deletion/Kptfile
@@ -4,11 +4,11 @@ metadata:
   name: app
 pipeline:
   mutators:
-  - image: gcr.io/kpt-fn/starlark:unstable
-    configPath: starlark-httpbin.yaml
-  - image: gcr.io/kpt-fn/set-namespace:unstable
-    configMap:
-      namespace: staging
-  - image: gcr.io/kpt-fn/set-labels:unstable
-    configMap:
-      tier: backend
+    - image: gcr.io/kpt-fn/starlark:unstable
+      configPath: starlark-httpbin.yaml
+    - image: gcr.io/kpt-fn/set-namespace:unstable
+      configMap:
+        namespace: staging
+    - image: gcr.io/kpt-fn/set-label:unstable
+      configMap:
+        tier: backend
diff --git a/e2e/testdata/fn-render/resource-deletion/deployment_httpbin.yaml b/e2e/testdata/fn-render/resource-deletion/deployment_httpbin.yaml
index 0ef5d140..44bfc6be 100644
--- a/e2e/testdata/fn-render/resource-deletion/deployment_httpbin.yaml
+++ b/e2e/testdata/fn-render/resource-deletion/deployment_httpbin.yaml
@@ -11,15 +11,14 @@
 # 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: httpbin
+  namespace: staging
   labels:
     app: backend
     tier: db
-  name: httpbin
-  namespace: staging
 spec:
   replicas: 4
   selector:
@@ -33,5 +32,5 @@ spec:
         tier: db
     spec:
       containers:
-      - image: kennethreitz/httpbin
-        name: httpbin
+        - name: httpbin
+          image: kennethreitz/httpbin
diff --git a/e2e/testdata/fn-render/resource-deletion/starlark-httpbin.yaml b/e2e/testdata/fn-render/resource-deletion/starlark-httpbin.yaml
index 9c8a441f..3de07cf7 100644
--- a/e2e/testdata/fn-render/resource-deletion/starlark-httpbin.yaml
+++ b/e2e/testdata/fn-render/resource-deletion/starlark-httpbin.yaml
@@ -11,7 +11,6 @@
 # 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: fn.kpt.dev/v1alpha1
 kind: StarlarkRun
 metadata:
@@ -20,6 +19,6 @@ source: |
   # filter to return if resource is HTTPBin resource
   def isHTTPBin(r):
     return r["apiVersion"] == "apps/v1" and r["kind"] == "Deployment" and r["metadata"]["name"] == "httpbin"
-  
+
   # filter out the httpbin deployment
   ctx.resource_list["items"] = [r for r in ctx.resource_list["items"] if not isHTTPBin(r)]
diff --git a/e2e/testdata/fn-render/resource-has-pkgname-prefix/.expected/diff.patch b/e2e/testdata/fn-render/resource-has-pkgname-prefix/.expected/diff.patch
index 8acc5bea..6f2d4b61 100644
--- a/e2e/testdata/fn-render/resource-has-pkgname-prefix/.expected/diff.patch
+++ b/e2e/testdata/fn-render/resource-has-pkgname-prefix/.expected/diff.patch
@@ -1,8 +1,8 @@
 diff --git a/mysql/mysql-deployment.yaml b/mysql/mysql-deployment.yaml
-index eb6a122..1fecfa4 100644
+index 173c2e1..1fecfa4 100644
 --- a/mysql/mysql-deployment.yaml
 +++ b/mysql/mysql-deployment.yaml
-@@ -18,14 +18,16 @@ metadata:
+@@ -18,8 +18,10 @@ metadata:
    labels:
      app: wordpress
    annotations:
@@ -13,17 +13,9 @@ index eb6a122..1fecfa4 100644
 +    projectId: PROJECT_ID # kpt-set: ${gcloud.core.project}
 +    teamname: YOURTEAM # kpt-set: ${teamname}
  spec:
--  ports:
--  - port: 3306
    selector:
      app: wordpress
-     tier: mysql
-+  ports:
-+    - port: 3306
-   clusterIP: None
- ---
- apiVersion: v1
-@@ -35,14 +37,16 @@ metadata:
+@@ -35,8 +37,10 @@ metadata:
    labels:
      app: wordpress
    annotations:
@@ -34,17 +26,9 @@ index eb6a122..1fecfa4 100644
 +    projectId: PROJECT_ID # kpt-set: ${gcloud.core.project}
 +    teamname: YOURTEAM # kpt-set: ${teamname}
  spec:
--  accessModes:
--  - ReadWriteOnce
    resources:
      requests:
-       storage: 20Gi
-+  accessModes:
-+    - ReadWriteOnce
- ---
- apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
- kind: Deployment
-@@ -51,37 +55,42 @@ metadata:
+@@ -51,8 +55,10 @@ metadata:
    labels:
      app: wordpress
    annotations:
@@ -57,12 +41,7 @@ index eb6a122..1fecfa4 100644
  spec:
    selector:
      matchLabels:
-       app: wordpress
-       tier: mysql
--  strategy:
--    type: Recreate
-   template:
-     metadata:
+@@ -63,6 +69,9 @@ spec:
        labels:
          app: wordpress
          tier: mysql
@@ -71,48 +50,12 @@ index eb6a122..1fecfa4 100644
 +        foo: bar
      spec:
        containers:
--      - image: mysql:5.6 # kpt-set: ${ms-image}:${ms-tag}
--        name: mysql
--        env:
--        - name: MYSQL_ROOT_PASSWORD
--          valueFrom:
--            secretKeyRef:
--              name: mysql-pass
--              key: password
--        ports:
--        - containerPort: 3306
--          name: mysql
--        volumeMounts:
--        - name: mysql-persistent-storage
--          mountPath: /var/lib/mysql
-+        - name: mysql
-+          image: mysql:5.6 # kpt-set: ${ms-image}:${ms-tag}
-+          ports:
-+            - name: mysql
-+              containerPort: 3306
-+          env:
-+            - name: MYSQL_ROOT_PASSWORD
-+              valueFrom:
-+                secretKeyRef:
-+                  name: mysql-pass
-+                  key: password
-+          volumeMounts:
-+            - name: mysql-persistent-storage
-+              mountPath: /var/lib/mysql
-       volumes:
--      - name: mysql-persistent-storage
--        persistentVolumeClaim:
--          claimName: mysql-pv-claim
-+        - name: mysql-persistent-storage
-+          persistentVolumeClaim:
-+            claimName: mysql-pv-claim
-+  strategy:
-+    type: Recreate
+         - name: mysql
 diff --git a/wordpress-deployment.yaml b/wordpress-deployment.yaml
-index 693b8d4..3cbda32 100644
+index f7bdc2a..3cbda32 100644
 --- a/wordpress-deployment.yaml
 +++ b/wordpress-deployment.yaml
-@@ -16,15 +16,16 @@ metadata:
+@@ -16,8 +16,9 @@ metadata:
    labels:
      app: wordpress
    annotations:
@@ -122,19 +65,9 @@ index 693b8d4..3cbda32 100644
 +    projectId: PROJECT_ID # kpt-set: ${gcloud.core.project}
 +    teamname: YOURTEAM # kpt-set: ${teamname}
  spec:
--  ports:
--  - port: 80
-+  type: LoadBalancer
+   type: LoadBalancer
    selector:
-     app: wordpress
-     tier: frontend
--  type: LoadBalancer
-+  ports:
-+    - port: 80
- ---
- apiVersion: v1
- kind: PersistentVolumeClaim
-@@ -33,14 +34,15 @@ metadata:
+@@ -33,8 +34,9 @@ metadata:
    labels:
      app: wordpress
    annotations:
@@ -144,17 +77,9 @@ index 693b8d4..3cbda32 100644
 +    projectId: PROJECT_ID # kpt-set: ${gcloud.core.project}
 +    teamname: YOURTEAM # kpt-set: ${teamname}
  spec:
--  accessModes:
--  - ReadWriteOnce
    resources:
      requests:
-       storage: 20Gi
-+  accessModes:
-+    - ReadWriteOnce
- ---
- apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
- kind: Deployment
-@@ -49,39 +51,42 @@ metadata:
+@@ -49,8 +51,9 @@ metadata:
    labels:
      app: wordpress
    annotations:
@@ -166,12 +91,7 @@ index 693b8d4..3cbda32 100644
  spec:
    selector:
      matchLabels:
-       app: wordpress
-       tier: frontend
--  strategy:
--    type: Recreate
-   template:
-     metadata:
+@@ -61,6 +64,8 @@ spec:
        labels:
          app: wordpress
          tier: frontend
@@ -179,44 +99,4 @@ index 693b8d4..3cbda32 100644
 +        abc: def
      spec:
        containers:
--      - image: wordpress:4.8-apache # kpt-set: ${wp-image}:${wp-tag}
--        name: wordpress
--        env:
--        - name: WORDPRESS_DB_HOST
--          value: wordpress-mysql
--        - name: WORDPRESS_DB_PASSWORD
--          valueFrom:
--            secretKeyRef:
--              name: mysql-pass
--              key: password
--        ports:
--        - containerPort: 80
--          name: wordpress
--        volumeMounts:
--        - name: wordpress-persistent-storage
--          mountPath: /var/www/html
-+        - name: wordpress
-+          image: wordpress:4.8-apache # kpt-set: ${wp-image}:${wp-tag}
-+          ports:
-+            - name: wordpress
-+              containerPort: 80
-+          env:
-+            - name: WORDPRESS_DB_HOST
-+              value: wordpress-mysql
-+            - name: WORDPRESS_DB_PASSWORD
-+              valueFrom:
-+                secretKeyRef:
-+                  name: mysql-pass
-+                  key: password
-+          volumeMounts:
-+            - name: wordpress-persistent-storage
-+              mountPath: /var/www/html
-       volumes:
--      - name: wordpress-persistent-storage
--        persistentVolumeClaim:
--          claimName: wp-pv-claim
-+        - name: wordpress-persistent-storage
-+          persistentVolumeClaim:
-+            claimName: wp-pv-claim
-+  strategy:
-+    type: Recreate
+         - name: wordpress
\ No newline at end of file
diff --git a/e2e/testdata/fn-render/resource-has-pkgname-prefix/Kptfile b/e2e/testdata/fn-render/resource-has-pkgname-prefix/Kptfile
index ff7a942a..04a1392a 100644
--- a/e2e/testdata/fn-render/resource-has-pkgname-prefix/Kptfile
+++ b/e2e/testdata/fn-render/resource-has-pkgname-prefix/Kptfile
@@ -4,6 +4,6 @@ metadata:
   name: wordpress
 pipeline:
   mutators:
-  - image: gcr.io/kpt-fn/set-annotations:unstable
-    configMap:
-      abc: def
+    - image: gcr.io/kpt-fn/set-annotation:unstable
+      configMap:
+        abc: def
diff --git a/e2e/testdata/fn-render/resource-has-pkgname-prefix/mysql/Kptfile b/e2e/testdata/fn-render/resource-has-pkgname-prefix/mysql/Kptfile
index b720deab..0a4fc172 100644
--- a/e2e/testdata/fn-render/resource-has-pkgname-prefix/mysql/Kptfile
+++ b/e2e/testdata/fn-render/resource-has-pkgname-prefix/mysql/Kptfile
@@ -4,6 +4,6 @@ metadata:
   name: mysql
 pipeline:
   mutators:
-  - image: gcr.io/kpt-fn/set-annotations:v0.1
-    configMap:
-      foo: bar
+    - image: gcr.io/kpt-fn/set-annotation:v0.1
+      configMap:
+        foo: bar
diff --git a/e2e/testdata/fn-render/resource-has-pkgname-prefix/mysql/mysql-deployment.yaml b/e2e/testdata/fn-render/resource-has-pkgname-prefix/mysql/mysql-deployment.yaml
index eb6a1227..173c2e11 100644
--- a/e2e/testdata/fn-render/resource-has-pkgname-prefix/mysql/mysql-deployment.yaml
+++ b/e2e/testdata/fn-render/resource-has-pkgname-prefix/mysql/mysql-deployment.yaml
@@ -21,11 +21,11 @@ metadata:
     projectId: 'PROJECT_ID' # kpt-set: ${gcloud.core.project}
     teamname: 'YOURTEAM' # kpt-set: ${teamname}
 spec:
-  ports:
-  - port: 3306
   selector:
     app: wordpress
     tier: mysql
+  ports:
+    - port: 3306
   clusterIP: None
 ---
 apiVersion: v1
@@ -38,11 +38,11 @@ metadata:
     projectId: 'PROJECT_ID' # kpt-set: ${gcloud.core.project}
     teamname: 'YOURTEAM' # kpt-set: ${teamname}
 spec:
-  accessModes:
-  - ReadWriteOnce
   resources:
     requests:
       storage: 20Gi
+  accessModes:
+    - ReadWriteOnce
 ---
 apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
 kind: Deployment
@@ -58,8 +58,6 @@ spec:
     matchLabels:
       app: wordpress
       tier: mysql
-  strategy:
-    type: Recreate
   template:
     metadata:
       labels:
@@ -67,21 +65,23 @@ spec:
         tier: mysql
     spec:
       containers:
-      - image: mysql:5.6 # kpt-set: ${ms-image}:${ms-tag}
-        name: mysql
-        env:
-        - name: MYSQL_ROOT_PASSWORD
-          valueFrom:
-            secretKeyRef:
-              name: mysql-pass
-              key: password
-        ports:
-        - containerPort: 3306
-          name: mysql
-        volumeMounts:
-        - name: mysql-persistent-storage
-          mountPath: /var/lib/mysql
+        - name: mysql
+          image: mysql:5.6 # kpt-set: ${ms-image}:${ms-tag}
+          ports:
+            - name: mysql
+              containerPort: 3306
+          env:
+            - name: MYSQL_ROOT_PASSWORD
+              valueFrom:
+                secretKeyRef:
+                  name: mysql-pass
+                  key: password
+          volumeMounts:
+            - name: mysql-persistent-storage
+              mountPath: /var/lib/mysql
       volumes:
-      - name: mysql-persistent-storage
-        persistentVolumeClaim:
-          claimName: mysql-pv-claim
+        - name: mysql-persistent-storage
+          persistentVolumeClaim:
+            claimName: mysql-pv-claim
+  strategy:
+    type: Recreate
diff --git a/e2e/testdata/fn-render/resource-has-pkgname-prefix/wordpress-deployment.yaml b/e2e/testdata/fn-render/resource-has-pkgname-prefix/wordpress-deployment.yaml
index 693b8d4c..f7bdc2ae 100644
--- a/e2e/testdata/fn-render/resource-has-pkgname-prefix/wordpress-deployment.yaml
+++ b/e2e/testdata/fn-render/resource-has-pkgname-prefix/wordpress-deployment.yaml
@@ -19,12 +19,12 @@ metadata:
     projectId: 'PROJECT_ID' # kpt-set: ${gcloud.core.project}
     teamname: 'YOURTEAM' # kpt-set: ${teamname}
 spec:
-  ports:
-  - port: 80
+  type: LoadBalancer
   selector:
     app: wordpress
     tier: frontend
-  type: LoadBalancer
+  ports:
+    - port: 80
 ---
 apiVersion: v1
 kind: PersistentVolumeClaim
@@ -36,11 +36,11 @@ metadata:
     projectId: 'PROJECT_ID' # kpt-set: ${gcloud.core.project}
     teamname: 'YOURTEAM' # kpt-set: ${teamname}
 spec:
-  accessModes:
-  - ReadWriteOnce
   resources:
     requests:
       storage: 20Gi
+  accessModes:
+    - ReadWriteOnce
 ---
 apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
 kind: Deployment
@@ -56,8 +56,6 @@ spec:
     matchLabels:
       app: wordpress
       tier: frontend
-  strategy:
-    type: Recreate
   template:
     metadata:
       labels:
@@ -65,23 +63,25 @@ spec:
         tier: frontend
     spec:
       containers:
-      - image: wordpress:4.8-apache # kpt-set: ${wp-image}:${wp-tag}
-        name: wordpress
-        env:
-        - name: WORDPRESS_DB_HOST
-          value: wordpress-mysql
-        - name: WORDPRESS_DB_PASSWORD
-          valueFrom:
-            secretKeyRef:
-              name: mysql-pass
-              key: password
-        ports:
-        - containerPort: 80
-          name: wordpress
-        volumeMounts:
-        - name: wordpress-persistent-storage
-          mountPath: /var/www/html
+        - name: wordpress
+          image: wordpress:4.8-apache # kpt-set: ${wp-image}:${wp-tag}
+          ports:
+            - name: wordpress
+              containerPort: 80
+          env:
+            - name: WORDPRESS_DB_HOST
+              value: wordpress-mysql
+            - name: WORDPRESS_DB_PASSWORD
+              valueFrom:
+                secretKeyRef:
+                  name: mysql-pass
+                  key: password
+          volumeMounts:
+            - name: wordpress-persistent-storage
+              mountPath: /var/www/html
       volumes:
-      - name: wordpress-persistent-storage
-        persistentVolumeClaim:
-          claimName: wp-pv-claim
+        - name: wordpress-persistent-storage
+          persistentVolumeClaim:
+            claimName: wp-pv-claim
+  strategy:
+    type: Recreate
diff --git a/e2e/testdata/fn-render/structured-results-from-muiltiple-fns/Kptfile b/e2e/testdata/fn-render/structured-results-from-muiltiple-fns/Kptfile
index be77a97c..e555cf57 100644
--- a/e2e/testdata/fn-render/structured-results-from-muiltiple-fns/Kptfile
+++ b/e2e/testdata/fn-render/structured-results-from-muiltiple-fns/Kptfile
@@ -7,5 +7,5 @@ pipeline:
     - image: gcr.io/kpt-fn/enforce-gatekeeper:unstable
     - image: gcr.io/kpt-fn/kubeval:unstable
       configMap:
-        strict: "true"
         ignore_missing_schemas: "true"
+        strict: "true"
diff --git a/e2e/testdata/fn-render/structured-results-from-muiltiple-fns/resources.yaml b/e2e/testdata/fn-render/structured-results-from-muiltiple-fns/resources.yaml
index 17a9f298..de1baf2f 100644
--- a/e2e/testdata/fn-render/structured-results-from-muiltiple-fns/resources.yaml
+++ b/e2e/testdata/fn-render/structured-results-from-muiltiple-fns/resources.yaml
@@ -11,7 +11,6 @@
 # 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:
@@ -53,7 +52,7 @@ kind: K8sConfigMapKeysRulesV1
 metadata:
   name: no-secrets-in-configmap
 spec:
-  enforcementAction: warn  # create warning instead of errors
+  enforcementAction: warn # create warning instead of errors
   match:
     kinds:
       - apiGroups:
@@ -69,7 +68,7 @@ kind: K8sConfigMapKeysRulesV1
 metadata:
   name: no-sensitive-data-in-configmap
 spec:
-  enforcementAction: dryrun  # create info instead of errors
+  enforcementAction: dryrun # create info instead of errors
   match:
     kinds:
       - apiGroups:
@@ -86,5 +85,5 @@ metadata:
   name: some-secret
   namespace: default
 data:
-  private_key: sensitive data goes here
   less_sensitive_key: less sensitive data goes here
+  private_key: sensitive data goes here
diff --git a/e2e/testdata/fn-render/subpkg-fn-failure/Kptfile b/e2e/testdata/fn-render/subpkg-fn-failure/Kptfile
index 23a9f778..e45ddb33 100644
--- a/e2e/testdata/fn-render/subpkg-fn-failure/Kptfile
+++ b/e2e/testdata/fn-render/subpkg-fn-failure/Kptfile
@@ -4,11 +4,11 @@ metadata:
   name: app
 pipeline:
   mutators:
-  - image: gcr.io/kpt-fn/starlark:unstable
-    configPath: starlark-httpbin.yaml
-  - image: gcr.io/kpt-fn/set-namespace:unstable
-    configMap:
-      namespace: staging
-  - image: gcr.io/kpt-fn/set-labels:unstable
-    configMap:
-      tier: backend
+    - image: gcr.io/kpt-fn/starlark:unstable
+      configPath: starlark-httpbin.yaml
+    - image: gcr.io/kpt-fn/set-namespace:unstable
+      configMap:
+        namespace: staging
+    - image: gcr.io/kpt-fn/set-label:unstable
+      configMap:
+        tier: backend
diff --git a/e2e/testdata/fn-render/subpkg-fn-failure/db/Kptfile b/e2e/testdata/fn-render/subpkg-fn-failure/db/Kptfile
index 3e6d64f4..98f34e1d 100644
--- a/e2e/testdata/fn-render/subpkg-fn-failure/db/Kptfile
+++ b/e2e/testdata/fn-render/subpkg-fn-failure/db/Kptfile
@@ -4,11 +4,11 @@ metadata:
   name: db
 pipeline:
   mutators:
-  - image: gcr.io/kpt-fn/starlark:unstable
-    configPath: statefulset-filter.yaml
-  - image: gcr.io/kpt-fn/set-namespace:unstable
-    configMap:
-      namespace: db
-  - image: gcr.io/kpt-fn/set-labels:unstable
-    configMap:
-      app: backend
+    - image: gcr.io/kpt-fn/starlark:unstable
+      configPath: statefulset-filter.yaml
+    - image: gcr.io/kpt-fn/set-namespace:unstable
+      configMap:
+        namespace: db
+    - image: gcr.io/kpt-fn/set-label:unstable
+      configMap:
+        app: backend
diff --git a/e2e/testdata/fn-render/subpkg-fn-failure/db/resources.yaml b/e2e/testdata/fn-render/subpkg-fn-failure/db/resources.yaml
index 3aca2764..22701718 100644
--- a/e2e/testdata/fn-render/subpkg-fn-failure/db/resources.yaml
+++ b/e2e/testdata/fn-render/subpkg-fn-failure/db/resources.yaml
@@ -11,7 +11,6 @@
 # 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:
diff --git a/e2e/testdata/fn-render/subpkg-fn-failure/db/statefulset-filter.yaml b/e2e/testdata/fn-render/subpkg-fn-failure/db/statefulset-filter.yaml
index 23c14f35..2ef98fd8 100644
--- a/e2e/testdata/fn-render/subpkg-fn-failure/db/statefulset-filter.yaml
+++ b/e2e/testdata/fn-render/subpkg-fn-failure/db/statefulset-filter.yaml
@@ -11,7 +11,6 @@
 # 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: fn.kpt.dev/v1alpha1
 kind: StarlarkRun
 metadata:
@@ -22,6 +21,6 @@ source: |
   def isStatefulSet(r):
     return r["apiVersion"] == "apps/v1" and
     r["kind"] == "StatefulSet"
-  
+
   # filter out statefulsets
   ctx.resource_list["items"] = [r for r in ctx.resource_list["items"] if not isStatefulSet(r)]
diff --git a/e2e/testdata/fn-render/subpkg-fn-failure/deployment_httpbin.yaml b/e2e/testdata/fn-render/subpkg-fn-failure/deployment_httpbin.yaml
index 0ef5d140..44bfc6be 100644
--- a/e2e/testdata/fn-render/subpkg-fn-failure/deployment_httpbin.yaml
+++ b/e2e/testdata/fn-render/subpkg-fn-failure/deployment_httpbin.yaml
@@ -11,15 +11,14 @@
 # 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: httpbin
+  namespace: staging
   labels:
     app: backend
     tier: db
-  name: httpbin
-  namespace: staging
 spec:
   replicas: 4
   selector:
@@ -33,5 +32,5 @@ spec:
         tier: db
     spec:
       containers:
-      - image: kennethreitz/httpbin
-        name: httpbin
+        - name: httpbin
+          image: kennethreitz/httpbin
diff --git a/e2e/testdata/fn-render/subpkg-fn-failure/resources.yaml b/e2e/testdata/fn-render/subpkg-fn-failure/resources.yaml
index cf4a2cf0..6590f6ca 100644
--- a/e2e/testdata/fn-render/subpkg-fn-failure/resources.yaml
+++ b/e2e/testdata/fn-render/subpkg-fn-failure/resources.yaml
@@ -11,7 +11,6 @@
 # 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:
diff --git a/e2e/testdata/fn-render/subpkg-fn-failure/starlark-httpbin.yaml b/e2e/testdata/fn-render/subpkg-fn-failure/starlark-httpbin.yaml
index a5716688..5a17604f 100644
--- a/e2e/testdata/fn-render/subpkg-fn-failure/starlark-httpbin.yaml
+++ b/e2e/testdata/fn-render/subpkg-fn-failure/starlark-httpbin.yaml
@@ -11,7 +11,6 @@
 # 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: fn.kpt.dev/v1alpha1
 kind: StarlarkRun
 metadata:
@@ -21,6 +20,6 @@ source: |
   def isHTTPBin(r):
     return r["apiVersion"] == "apps/v1" and
     r["kind"] == "Deployment" and r["metadata"]["name"] == "httpbin"
-  
+
   # filter out the httpbin deployment
   ctx.resource_list["items"] = [r for r in ctx.resource_list["items"] if not isHTTPBin(r)]
diff --git a/e2e/testdata/fn-render/subpkg-has-invalid-kptfile/Kptfile b/e2e/testdata/fn-render/subpkg-has-invalid-kptfile/Kptfile
index c002959a..1a5dd33c 100644
--- a/e2e/testdata/fn-render/subpkg-has-invalid-kptfile/Kptfile
+++ b/e2e/testdata/fn-render/subpkg-has-invalid-kptfile/Kptfile
@@ -4,9 +4,9 @@ metadata:
   name: app
 pipeline:
   mutators:
-  - image: gcr.io/kpt-fn/set-namespace:unstable
-    configMap:
-      namespace: staging
-  - image: gcr.io/kpt-fn/set-labels:unstable
-    configMap:
-      tier: backend
+    - image: gcr.io/kpt-fn/set-namespace:unstable
+      configMap:
+        namespace: staging
+    - image: gcr.io/kpt-fn/set-label:unstable
+      configMap:
+        tier: backend
diff --git a/e2e/testdata/fn-render/subpkg-has-invalid-kptfile/db/Kptfile b/e2e/testdata/fn-render/subpkg-has-invalid-kptfile/db/Kptfile
index ea6955e6..a414064a 100644
--- a/e2e/testdata/fn-render/subpkg-has-invalid-kptfile/db/Kptfile
+++ b/e2e/testdata/fn-render/subpkg-has-invalid-kptfile/db/Kptfile
@@ -4,12 +4,12 @@ metadata:
   name: db
 pipeline:
   mutators:
-  - image: gcr.io/kpt-fn/starlark:unstable
-# malformed Kptfile
-configPath: statefulset-filter.yaml
-  - image: gcr.io/kpt-fn/set-namespace:unstable
-    configMap:
-      namespace: db
-  - image: gcr.io/kpt-fn/set-labels:unstable
-    configMap:
-      app: backend
+    - image: gcr.io/kpt-fn/starlark:unstable
+    # malformed Kptfile
+  configPath: statefulset-filter.yaml
+    - image: gcr.io/kpt-fn/set-namespace:unstable
+      configMap:
+        namespace: db
+    - image: gcr.io/kpt-fn/set-label:unstable
+      configMap:
+        app: backend
diff --git a/e2e/testdata/fn-render/subpkg-has-invalid-kptfile/db/resources.yaml b/e2e/testdata/fn-render/subpkg-has-invalid-kptfile/db/resources.yaml
index 3aca2764..22701718 100644
--- a/e2e/testdata/fn-render/subpkg-has-invalid-kptfile/db/resources.yaml
+++ b/e2e/testdata/fn-render/subpkg-has-invalid-kptfile/db/resources.yaml
@@ -11,7 +11,6 @@
 # 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:
diff --git a/e2e/testdata/fn-render/subpkg-has-invalid-kptfile/db/statefulset-filter.yaml b/e2e/testdata/fn-render/subpkg-has-invalid-kptfile/db/statefulset-filter.yaml
index e1cf9f09..b1beb4b2 100644
--- a/e2e/testdata/fn-render/subpkg-has-invalid-kptfile/db/statefulset-filter.yaml
+++ b/e2e/testdata/fn-render/subpkg-has-invalid-kptfile/db/statefulset-filter.yaml
@@ -11,7 +11,6 @@
 # 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: fn.kpt.dev/v1alpha1
 kind: StarlarkRun
 metadata:
@@ -21,6 +20,6 @@ source: |
   def isStatefulSet(r):
     return r["apiVersion"] == "apps/v1" and
     r["kind"] == "StatefulSet"
-  
+
   # filter out statefulsets
   ctx.resource_list["items"] = [r for r in ctx.resource_list["items"] if not isStatefulSet(r)]
diff --git a/e2e/testdata/fn-render/subpkg-has-invalid-kptfile/resources.yaml b/e2e/testdata/fn-render/subpkg-has-invalid-kptfile/resources.yaml
index cf4a2cf0..6590f6ca 100644
--- a/e2e/testdata/fn-render/subpkg-has-invalid-kptfile/resources.yaml
+++ b/e2e/testdata/fn-render/subpkg-has-invalid-kptfile/resources.yaml
@@ -11,7 +11,6 @@
 # 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:
diff --git a/e2e/testdata/fn-render/subpkg-resource-deletion/.expected/diff.patch b/e2e/testdata/fn-render/subpkg-resource-deletion/.expected/diff.patch
index 943532cb..a6dba647 100644
--- a/e2e/testdata/fn-render/subpkg-resource-deletion/.expected/diff.patch
+++ b/e2e/testdata/fn-render/subpkg-resource-deletion/.expected/diff.patch
@@ -1,8 +1,8 @@
 diff --git a/db/resources.yaml b/db/resources.yaml
-index 3aca276..9dabb18 100644
+index 2270171..9dabb18 100644
 --- a/db/resources.yaml
 +++ b/db/resources.yaml
-@@ -1,27 +1,10 @@
+@@ -1,26 +1,10 @@
 -# Copyright 2021 Google LLC
 -#
 -# Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,7 +16,6 @@ index 3aca276..9dabb18 100644
 -# 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:
@@ -36,10 +35,10 @@ index 3aca276..9dabb18 100644
    image: nginx:1.2.3
 diff --git a/deployment_httpbin.yaml b/deployment_httpbin.yaml
 deleted file mode 100644
-index 0ef5d14..0000000
+index 44bfc6b..0000000
 --- a/deployment_httpbin.yaml
 +++ /dev/null
-@@ -1,37 +0,0 @@
+@@ -1,36 +0,0 @@
 -# Copyright 2021 Google LLC
 -#
 -# Licensed under the Apache License, Version 2.0 (the "License");
@@ -53,15 +52,14 @@ index 0ef5d14..0000000
 -# 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: httpbin
+-  namespace: staging
 -  labels:
 -    app: backend
 -    tier: db
--  name: httpbin
--  namespace: staging
 -spec:
 -  replicas: 4
 -  selector:
@@ -75,8 +73,8 @@ index 0ef5d14..0000000
 -        tier: db
 -    spec:
 -      containers:
--      - image: kennethreitz/httpbin
--        name: httpbin
+-        - name: httpbin
+-          image: kennethreitz/httpbin
 diff --git a/resources.yaml b/resources.yaml
 index 6590f6c..b5b54e0 100644
 --- a/resources.yaml
@@ -96,4 +94,4 @@ index 6590f6c..b5b54e0 100644
 +  template:
 +    metadata:
 +      labels:
-+        tier: backend
++        tier: backend
\ No newline at end of file
diff --git a/e2e/testdata/fn-render/subpkg-resource-deletion/Kptfile b/e2e/testdata/fn-render/subpkg-resource-deletion/Kptfile
index 23a9f778..e45ddb33 100644
--- a/e2e/testdata/fn-render/subpkg-resource-deletion/Kptfile
+++ b/e2e/testdata/fn-render/subpkg-resource-deletion/Kptfile
@@ -4,11 +4,11 @@ metadata:
   name: app
 pipeline:
   mutators:
-  - image: gcr.io/kpt-fn/starlark:unstable
-    configPath: starlark-httpbin.yaml
-  - image: gcr.io/kpt-fn/set-namespace:unstable
-    configMap:
-      namespace: staging
-  - image: gcr.io/kpt-fn/set-labels:unstable
-    configMap:
-      tier: backend
+    - image: gcr.io/kpt-fn/starlark:unstable
+      configPath: starlark-httpbin.yaml
+    - image: gcr.io/kpt-fn/set-namespace:unstable
+      configMap:
+        namespace: staging
+    - image: gcr.io/kpt-fn/set-label:unstable
+      configMap:
+        tier: backend
diff --git a/e2e/testdata/fn-render/subpkg-resource-deletion/db/Kptfile b/e2e/testdata/fn-render/subpkg-resource-deletion/db/Kptfile
index 3e6d64f4..98f34e1d 100644
--- a/e2e/testdata/fn-render/subpkg-resource-deletion/db/Kptfile
+++ b/e2e/testdata/fn-render/subpkg-resource-deletion/db/Kptfile
@@ -4,11 +4,11 @@ metadata:
   name: db
 pipeline:
   mutators:
-  - image: gcr.io/kpt-fn/starlark:unstable
-    configPath: statefulset-filter.yaml
-  - image: gcr.io/kpt-fn/set-namespace:unstable
-    configMap:
-      namespace: db
-  - image: gcr.io/kpt-fn/set-labels:unstable
-    configMap:
-      app: backend
+    - image: gcr.io/kpt-fn/starlark:unstable
+      configPath: statefulset-filter.yaml
+    - image: gcr.io/kpt-fn/set-namespace:unstable
+      configMap:
+        namespace: db
+    - image: gcr.io/kpt-fn/set-label:unstable
+      configMap:
+        app: backend
diff --git a/e2e/testdata/fn-render/subpkg-resource-deletion/db/resources.yaml b/e2e/testdata/fn-render/subpkg-resource-deletion/db/resources.yaml
index 3aca2764..22701718 100644
--- a/e2e/testdata/fn-render/subpkg-resource-deletion/db/resources.yaml
+++ b/e2e/testdata/fn-render/subpkg-resource-deletion/db/resources.yaml
@@ -11,7 +11,6 @@
 # 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:
diff --git a/e2e/testdata/fn-render/subpkg-resource-deletion/db/statefulset-filter.yaml b/e2e/testdata/fn-render/subpkg-resource-deletion/db/statefulset-filter.yaml
index 02cf3a3e..7d006a87 100644
--- a/e2e/testdata/fn-render/subpkg-resource-deletion/db/statefulset-filter.yaml
+++ b/e2e/testdata/fn-render/subpkg-resource-deletion/db/statefulset-filter.yaml
@@ -11,7 +11,6 @@
 # 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: fn.kpt.dev/v1alpha1
 kind: StarlarkRun
 metadata:
@@ -20,6 +19,6 @@ source: |
   # filter to return if resource is statefulset kind
   def isStatefulSet(r):
     return r["apiVersion"] == "apps/v1" and r["kind"] == "StatefulSet"
-  
+
   # filter out statefulsets
   ctx.resource_list["items"] = [r for r in ctx.resource_list["items"] if not isStatefulSet(r)]
diff --git a/e2e/testdata/fn-render/subpkg-resource-deletion/deployment_httpbin.yaml b/e2e/testdata/fn-render/subpkg-resource-deletion/deployment_httpbin.yaml
index 0ef5d140..44bfc6be 100644
--- a/e2e/testdata/fn-render/subpkg-resource-deletion/deployment_httpbin.yaml
+++ b/e2e/testdata/fn-render/subpkg-resource-deletion/deployment_httpbin.yaml
@@ -11,15 +11,14 @@
 # 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: httpbin
+  namespace: staging
   labels:
     app: backend
     tier: db
-  name: httpbin
-  namespace: staging
 spec:
   replicas: 4
   selector:
@@ -33,5 +32,5 @@ spec:
         tier: db
     spec:
       containers:
-      - image: kennethreitz/httpbin
-        name: httpbin
+        - name: httpbin
+          image: kennethreitz/httpbin
diff --git a/e2e/testdata/fn-render/subpkg-resource-deletion/starlark-httpbin.yaml b/e2e/testdata/fn-render/subpkg-resource-deletion/starlark-httpbin.yaml
index 9c8a441f..3de07cf7 100644
--- a/e2e/testdata/fn-render/subpkg-resource-deletion/starlark-httpbin.yaml
+++ b/e2e/testdata/fn-render/subpkg-resource-deletion/starlark-httpbin.yaml
@@ -11,7 +11,6 @@
 # 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: fn.kpt.dev/v1alpha1
 kind: StarlarkRun
 metadata:
@@ -20,6 +19,6 @@ source: |
   # filter to return if resource is HTTPBin resource
   def isHTTPBin(r):
     return r["apiVersion"] == "apps/v1" and r["kind"] == "Deployment" and r["metadata"]["name"] == "httpbin"
-  
+
   # filter out the httpbin deployment
   ctx.resource_list["items"] = [r for r in ctx.resource_list["items"] if not isHTTPBin(r)]
diff --git a/e2e/testdata/fn-render/subpkgs/Kptfile b/e2e/testdata/fn-render/subpkgs/Kptfile
index ee08aa8c..783c0791 100644
--- a/e2e/testdata/fn-render/subpkgs/Kptfile
+++ b/e2e/testdata/fn-render/subpkgs/Kptfile
@@ -4,9 +4,9 @@ metadata:
   name: app-with-db
 pipeline:
   mutators:
-  - image: gcr.io/kpt-fn/set-namespace:unstable
-    configMap:
-      namespace: staging
-  - image: gcr.io/kpt-fn/set-labels:unstable
-    configMap:
-      tier: db
\ No newline at end of file
+    - image: gcr.io/kpt-fn/set-namespace:unstable
+      configMap:
+        namespace: staging
+    - image: gcr.io/kpt-fn/set-label:unstable
+      configMap:
+        tier: db
diff --git a/e2e/testdata/fn-render/subpkgs/db/Kptfile b/e2e/testdata/fn-render/subpkgs/db/Kptfile
index 0396b671..91e81375 100644
--- a/e2e/testdata/fn-render/subpkgs/db/Kptfile
+++ b/e2e/testdata/fn-render/subpkgs/db/Kptfile
@@ -4,9 +4,9 @@ metadata:
   name: db
 pipeline:
   mutators:
-  - image: gcr.io/kpt-fn/set-namespace:unstable
-    configMap:
-      namespace: db
-  - image: gcr.io/kpt-fn/set-labels:unstable
-    configMap:
-      app: backend
\ No newline at end of file
+    - image: gcr.io/kpt-fn/set-namespace:unstable
+      configMap:
+        namespace: db
+    - image: gcr.io/kpt-fn/set-label:unstable
+      configMap:
+        app: backend
diff --git a/e2e/testdata/fn-render/success-stdout/Kptfile b/e2e/testdata/fn-render/success-stdout/Kptfile
index 2b15e8f9..1a5dd33c 100644
--- a/e2e/testdata/fn-render/success-stdout/Kptfile
+++ b/e2e/testdata/fn-render/success-stdout/Kptfile
@@ -4,9 +4,9 @@ metadata:
   name: app
 pipeline:
   mutators:
-  - image: gcr.io/kpt-fn/set-namespace:unstable
-    configMap:
-      namespace: staging
-  - image: gcr.io/kpt-fn/set-labels:unstable
-    configMap:
-      tier: backend
\ No newline at end of file
+    - image: gcr.io/kpt-fn/set-namespace:unstable
+      configMap:
+        namespace: staging
+    - image: gcr.io/kpt-fn/set-label:unstable
+      configMap:
+        tier: backend
diff --git a/e2e/testdata/fn-render/validate-generated-resource/Kptfile b/e2e/testdata/fn-render/validate-generated-resource/Kptfile
index d75bb9d7..1590d625 100644
--- a/e2e/testdata/fn-render/validate-generated-resource/Kptfile
+++ b/e2e/testdata/fn-render/validate-generated-resource/Kptfile
@@ -4,8 +4,8 @@ metadata:
   name: db
 pipeline:
   mutators:
-    - image: gcr.io/kpt-fn/starlark:unstable # generates httpbin deployment
+    - image: gcr.io/kpt-fn/starlark:unstable
       configPath: starlark-httpbin-gen.yaml
   validators:
-    - image: gcr.io/kpt-fn/starlark:unstable # validates httpbin deployment exists
-      configPath: starlark-httpbin-val.yaml
\ No newline at end of file
+    - image: gcr.io/kpt-fn/starlark:unstable
+      configPath: starlark-httpbin-val.yaml
diff --git a/e2e/testdata/fn-render/validate-generated-resource/starlark-httpbin-gen.yaml b/e2e/testdata/fn-render/validate-generated-resource/starlark-httpbin-gen.yaml
index 14be713f..5c7eea79 100644
--- a/e2e/testdata/fn-render/validate-generated-resource/starlark-httpbin-gen.yaml
+++ b/e2e/testdata/fn-render/validate-generated-resource/starlark-httpbin-gen.yaml
@@ -11,12 +11,11 @@
 # 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: fn.kpt.dev/v1alpha1
 kind: StarlarkRun
 metadata:
   name: httpbin-gen
-source: |
+source: |-
   httpbin_deployment = {
      "apiVersion": "apps/v1",
      "kind": "Deployment",
@@ -53,4 +52,4 @@ source: |
 
     resources.append(httpbin_deployment)
 
-  ensure_httpbin(ctx.resource_list["items"])
\ No newline at end of file
+  ensure_httpbin(ctx.resource_list["items"])
diff --git a/e2e/testdata/fn-render/validate-generated-resource/starlark-httpbin-val.yaml b/e2e/testdata/fn-render/validate-generated-resource/starlark-httpbin-val.yaml
index 340c2a81..1dec086f 100644
--- a/e2e/testdata/fn-render/validate-generated-resource/starlark-httpbin-val.yaml
+++ b/e2e/testdata/fn-render/validate-generated-resource/starlark-httpbin-val.yaml
@@ -11,15 +11,14 @@
 # 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: fn.kpt.dev/v1alpha1
 kind: StarlarkRun
 metadata:
   name: httpbin-val
-source: |
+source: |-
   def contains_httpbin_resource(resource_list):
     for r in resource_list["items"]:
       if r["metadata"]["name"] == "httpbin" and r["kind"] == "Deployment":
         return
     fail("could not find httpbin deployment")
-  contains_httpbin_resource(ctx.resource_list)
\ No newline at end of file
+  contains_httpbin_resource(ctx.resource_list)
diff --git a/e2e/testdata/fn-render/validate-resource-failure/Kptfile b/e2e/testdata/fn-render/validate-resource-failure/Kptfile
index e9499650..6837f7d5 100644
--- a/e2e/testdata/fn-render/validate-resource-failure/Kptfile
+++ b/e2e/testdata/fn-render/validate-resource-failure/Kptfile
@@ -5,4 +5,4 @@ metadata:
 pipeline:
   validators:
     - image: gcr.io/kpt-fn/starlark:unstable # validates httpbin deployment exists
-      configPath: starlark-httpbin-val.yaml
\ No newline at end of file
+      configPath: starlark-httpbin-val.yaml
diff --git a/e2e/testdata/fn-render/validate-resource-failure/resources.yaml b/e2e/testdata/fn-render/validate-resource-failure/resources.yaml
index 10cd18bf..7a494c9c 100644
--- a/e2e/testdata/fn-render/validate-resource-failure/resources.yaml
+++ b/e2e/testdata/fn-render/validate-resource-failure/resources.yaml
@@ -11,7 +11,6 @@
 # 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:
diff --git a/e2e/testdata/fn-render/validate-resource-failure/starlark-httpbin-val.yaml b/e2e/testdata/fn-render/validate-resource-failure/starlark-httpbin-val.yaml
index 340c2a81..1dec086f 100644
--- a/e2e/testdata/fn-render/validate-resource-failure/starlark-httpbin-val.yaml
+++ b/e2e/testdata/fn-render/validate-resource-failure/starlark-httpbin-val.yaml
@@ -11,15 +11,14 @@
 # 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: fn.kpt.dev/v1alpha1
 kind: StarlarkRun
 metadata:
   name: httpbin-val
-source: |
+source: |-
   def contains_httpbin_resource(resource_list):
     for r in resource_list["items"]:
       if r["metadata"]["name"] == "httpbin" and r["kind"] == "Deployment":
         return
     fail("could not find httpbin deployment")
-  contains_httpbin_resource(ctx.resource_list)
\ No newline at end of file
+  contains_httpbin_resource(ctx.resource_list)
diff --git a/e2e/testdata/fn-render/validator-mutates-resources/Kptfile b/e2e/testdata/fn-render/validator-mutates-resources/Kptfile
index 8ed6d01f..837e76ce 100644
--- a/e2e/testdata/fn-render/validator-mutates-resources/Kptfile
+++ b/e2e/testdata/fn-render/validator-mutates-resources/Kptfile
@@ -4,7 +4,6 @@ metadata:
   name: app
 pipeline:
   validators:
-# mutating function is being used as validator
-    - image: gcr.io/kpt-fn/set-labels:unstable
+    - image: gcr.io/kpt-fn/set-label:unstable # mutator function is being used as validator
       configMap:
-        tier: backend
\ No newline at end of file
+        tier: backend
diff --git a/internal/cmddestroy/cmddestroy_test.go b/internal/cmddestroy/cmddestroy_test.go
new file mode 100644
index 00000000..e0e6a915
--- /dev/null
+++ b/internal/cmddestroy/cmddestroy_test.go
@@ -0,0 +1,116 @@
+// 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 cmddestroy
+
+import (
+	"path/filepath"
+	"testing"
+
+	"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/cli-runtime/pkg/genericclioptions"
+	cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
+	"sigs.k8s.io/cli-utils/pkg/inventory"
+)
+
+func TestCmd(t *testing.T) {
+	testCases := map[string]struct {
+		args                []string
+		namespace           string
+		inventory           *kptfilev1alpha2.Inventory
+		destroyCallbackFunc func(*testing.T, inventory.InventoryInfo)
+		expectedErrorMsg    string
+	}{
+		"invalid inventory policy": {
+			args: []string{
+				"--inventory-policy", "noSuchPolicy",
+			},
+			namespace: "testns",
+			destroyCallbackFunc: func(t *testing.T, _ inventory.InventoryInfo) {
+				t.FailNow()
+			},
+			expectedErrorMsg: "inventory policy must be one of strict, adopt",
+		},
+		"invalid output format": {
+			args: []string{
+				"--output", "foo",
+			},
+			namespace: "testns",
+			destroyCallbackFunc: func(t *testing.T, _ inventory.InventoryInfo) {
+				t.FailNow()
+			},
+			expectedErrorMsg: "unknown output type \"foo\"",
+		},
+		"fetches the correct inventory information from the Kptfile": {
+			args: []string{
+				"--inventory-policy", "adopt",
+				"--output", "events",
+			},
+			inventory: &kptfilev1alpha2.Inventory{
+				Namespace:   "my-ns",
+				Name:        "my-name",
+				InventoryID: "my-inv-id",
+			},
+			namespace: "testns",
+			destroyCallbackFunc: func(t *testing.T, inv inventory.InventoryInfo) {
+				assert.Equal(t, "my-ns", inv.Namespace())
+				assert.Equal(t, "my-name", inv.Name())
+				assert.Equal(t, "my-inv-id", inv.ID())
+			},
+		},
+	}
+
+	for tn, tc := range testCases {
+		t.Run(tn, func(t *testing.T) {
+			tf := cmdtesting.NewTestFactory().WithNamespace(tc.namespace)
+			defer tf.Cleanup()
+			ioStreams, _, _, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled
+
+			w, clean := testutil.SetupWorkspace(t)
+			defer clean()
+			kf := kptfileutil.DefaultKptfile(filepath.Base(w.WorkspaceDirectory))
+			kf.Inventory = tc.inventory
+			testutil.AddKptfileToWorkspace(t, w, kf)
+
+			revert := testutil.Chdir(t, w.WorkspaceDirectory)
+			defer revert()
+
+			rgProvider := live.NewResourceGroupProvider(tf)
+			rgLoader := live.NewResourceGroupManifestLoader(tf)
+
+			runner := NewRunner(fake.CtxWithNilPrinter(), rgProvider, rgLoader, ioStreams)
+			runner.Command.SetArgs(tc.args)
+			runner.destroyRunner = func(_ *Runner, inv inventory.InventoryInfo) error {
+				tc.destroyCallbackFunc(t, inv)
+				return nil
+			}
+			err := runner.Command.Execute()
+
+			// Check if there should be an error
+			if tc.expectedErrorMsg != "" {
+				if !assert.Error(t, err) {
+					t.FailNow()
+				}
+				assert.Contains(t, err.Error(), tc.expectedErrorMsg)
+				return
+			}
+			assert.NoError(t, err)
+		})
+	}
+}
diff --git a/internal/cmddiff/cmddiff_test.go b/internal/cmddiff/cmddiff_test.go
index 786ce662..3561dcf8 100644
--- a/internal/cmddiff/cmddiff_test.go
+++ b/internal/cmddiff/cmddiff_test.go
@@ -33,7 +33,6 @@ func TestMain(m *testing.M) {
 func TestCmdInvalidDiffType(t *testing.T) {
 	runner := cmddiff.NewRunner(fake.CtxWithNilPrinter(), "")
 	runner.C.SetArgs([]string{"--diff-type", "invalid"})
-	runner.C.SilenceErrors = true
 	err := runner.C.Execute()
 	assert.EqualError(t,
 		err,
@@ -43,7 +42,6 @@ func TestCmdInvalidDiffType(t *testing.T) {
 func TestCmdInvalidDiffTool(t *testing.T) {
 	runner := cmddiff.NewRunner(fake.CtxWithNilPrinter(), "")
 	runner.C.SetArgs([]string{"--diff-tool", "nodiff"})
-	runner.C.SilenceErrors = true
 	err := runner.C.Execute()
 	assert.EqualError(t,
 		err,
@@ -68,7 +66,6 @@ func TestCmdExecute(t *testing.T) {
 
 	runner := cmddiff.NewRunner(fake.CtxWithNilPrinter(), "")
 	runner.C.SetArgs([]string{dest, "--diff-type", "local"})
-	runner.C.SilenceErrors = true
 	err = runner.C.Execute()
 	assert.NoError(t, err)
 }
diff --git a/internal/cmdliveinit/cmdliveinit_test.go b/internal/cmdliveinit/cmdliveinit_test.go
index 863c84a3..0e155509 100644
--- a/internal/cmdliveinit/cmdliveinit_test.go
+++ b/internal/cmdliveinit/cmdliveinit_test.go
@@ -5,11 +5,14 @@ package cmdliveinit
 
 import (
 	"io/ioutil"
-	"os"
 	"path/filepath"
+	"regexp"
 	"testing"
 	"time"
 
+	"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/stretchr/testify/assert"
 	"k8s.io/cli-runtime/pkg/genericclioptions"
@@ -55,7 +58,7 @@ inventory:
 
 var testTime = time.Unix(5555555, 66666666)
 
-func TestKptInitOptions_generateID(t *testing.T) {
+func TestCmd_generateID(t *testing.T) {
 	testCases := map[string]struct {
 		namespace string
 		name      string
@@ -102,151 +105,147 @@ func TestKptInitOptions_generateID(t *testing.T) {
 	}
 }
 
-func TestKptInitOptions_updateKptfile(t *testing.T) {
+func TestCmd_Run(t *testing.T) {
 	testCases := map[string]struct {
-		kptfile     string
-		name        string
-		namespace   string
-		inventoryID string
-		force       bool
-		isError     bool
+		kptfile           string
+		name              string
+		namespace         string
+		inventoryID       string
+		force             bool
+		expectedErrorMsg  string
+		expectAutoGenID   bool
+		expectedInventory kptfilev1alpha2.Inventory
 	}{
-		"Empty inventory name is an error": {
-			kptfile:     kptFile,
-			name:        "",
-			namespace:   inventoryNamespace,
-			inventoryID: inventoryID,
-			force:       false,
-			isError:     true,
+		"Fields are defaulted if not provided": {
+			kptfile:         kptFile,
+			name:            "",
+			namespace:       "testns",
+			inventoryID:     "",
+			expectAutoGenID: true,
+			expectedInventory: kptfilev1alpha2.Inventory{
+				Namespace:   "testns",
+				Name:        "inventory-*",
+				InventoryID: "33ee4887f9638ef63efe71a9a9a632d3e9e2488e-*",
+			},
 		},
-		"Empty inventory namespace is an error": {
+		"Provided values are used": {
 			kptfile:     kptFile,
-			name:        inventoryName,
-			namespace:   "",
-			inventoryID: inventoryID,
-			force:       false,
-			isError:     true,
-		},
-		"Empty inventory id is an error": {
-			kptfile:     kptFile,
-			name:        inventoryName,
-			namespace:   inventoryNamespace,
-			inventoryID: "",
-			force:       false,
-			isError:     true,
+			name:        "my-pkg",
+			namespace:   "my-ns",
+			inventoryID: "my-inv-id",
+			expectedInventory: kptfilev1alpha2.Inventory{
+				Namespace:   "my-ns",
+				Name:        "my-pkg",
+				InventoryID: "my-inv-id",
+			},
 		},
 		"Kptfile with inventory already set is error": {
-			kptfile:     kptFileWithInventory,
-			name:        inventoryName,
-			namespace:   inventoryNamespace,
-			inventoryID: inventoryID,
-			force:       false,
-			isError:     true,
+			kptfile:          kptFileWithInventory,
+			name:             inventoryName,
+			namespace:        inventoryNamespace,
+			inventoryID:      inventoryID,
+			force:            false,
+			expectedErrorMsg: "inventory information already set for package",
 		},
-		"KptInitOptions default": {
-			kptfile:     kptFile,
-			name:        inventoryName,
-			namespace:   inventoryNamespace,
-			inventoryID: inventoryID,
-			force:       false,
-			isError:     false,
-		},
-		"KptInitOptions force sets inventory values when already set": {
+		"The force flag allows changing inventory information even if already set": {
 			kptfile:     kptFileWithInventory,
 			name:        inventoryName,
 			namespace:   inventoryNamespace,
 			inventoryID: inventoryID,
 			force:       true,
-			isError:     false,
+			expectedInventory: kptfilev1alpha2.Inventory{
+				Namespace:   inventoryNamespace,
+				Name:        inventoryName,
+				InventoryID: inventoryID,
+			},
 		},
 	}
 
 	for tn, tc := range testCases {
 		t.Run(tn, func(t *testing.T) {
 			// Set up fake test factory
-			tf := cmdtesting.NewTestFactory().WithNamespace("test-ns")
+			tf := cmdtesting.NewTestFactory().WithNamespace(tc.namespace)
 			defer tf.Cleanup()
 			ioStreams, _, _, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled
 
-			// Set up temp directory with Ktpfile
-			dir, err := ioutil.TempDir("", "kpt-init-options-test")
-			assert.NoError(t, err)
-			defer os.RemoveAll(dir)
-			p := filepath.Join(dir, "Kptfile")
-			err = ioutil.WriteFile(p, []byte(tc.kptfile), 0600)
-			assert.NoError(t, err)
+			w, clean := testutil.SetupWorkspace(t)
+			defer clean()
+			err := ioutil.WriteFile(filepath.Join(w.WorkspaceDirectory, kptfilev1alpha2.KptFileName),
+				[]byte(tc.kptfile), 0600)
+			if !assert.NoError(t, err) {
+				t.FailNow()
+			}
+
+			revert := testutil.Chdir(t, w.WorkspaceDirectory)
+			defer revert()
 
-			// Create KptInitOptions and call Run()
-			initOptions := NewKptInitOptions(tf, ioStreams)
-			initOptions.dir = dir
-			initOptions.Force = tc.force
-			initOptions.Name = tc.name
-			initOptions.namespace = tc.namespace
-			initOptions.InventoryID = tc.inventoryID
-			err = initOptions.updateKptfile()
+			runner := NewRunner(fake.CtxWithNilPrinter(), tf, ioStreams)
+			runner.namespace = tc.namespace
+			args := []string{
+				"--name", tc.name,
+				"--inventory-id", tc.inventoryID,
+			}
+			if tc.force {
+				args = append(args, "--force")
+			}
+			runner.Command.SetArgs(args)
+
+			err = runner.Command.Execute()
 
 			// Check if there should be an error
-			if tc.isError {
-				if err == nil {
-					t.Fatalf("expected error but received none")
+			if tc.expectedErrorMsg != "" {
+				if !assert.Error(t, err) {
+					t.FailNow()
 				}
+				assert.Contains(t, err.Error(), tc.expectedErrorMsg)
 				return
 			}
 
 			// Otherwise, validate the kptfile values
 			assert.NoError(t, err)
-			kf, err := kptfileutil.ReadFile(initOptions.dir)
+			kf, err := kptfileutil.ReadFile(w.WorkspaceDirectory)
 			assert.NoError(t, err)
-			assert.Equal(t, inventoryName, kf.Inventory.Name)
-			assert.Equal(t, inventoryNamespace, kf.Inventory.Namespace)
-			assert.Equal(t, inventoryID, kf.Inventory.InventoryID)
+			if !assert.NotNil(t, kf.Inventory) {
+				t.FailNow()
+			}
+			actualInv := *kf.Inventory
+			expectedInv := tc.expectedInventory
+			assertInventoryName(t, expectedInv.Name, actualInv.Name)
+			assert.Equal(t, expectedInv.Namespace, actualInv.Namespace)
+			if tc.expectAutoGenID {
+				assertGenInvID(t, actualInv.Name, actualInv.Namespace, actualInv.InventoryID)
+			} else {
+				assert.Equal(t, expectedInv.InventoryID, actualInv.InventoryID)
+			}
 		})
 	}
 }
 
-func TestKptInit_noargs(t *testing.T) {
-	testCases := map[string]struct {
-		kptfile     string
-		name        string
-		namespace   string
-		inventoryID string
-	}{
-		"Empty inventory name is an error": {
-			kptfile:     kptFile,
-			name:        "",
-			namespace:   inventoryNamespace,
-			inventoryID: inventoryID,
-		},
+func assertInventoryName(t *testing.T, expected, actual string) bool {
+	re := regexp.MustCompile(`^inventory-[0-9]+$`)
+	if expected == "inventory-*" {
+		if re.MatchString(actual) {
+			return true
+		}
+		t.Errorf("expected value on the format 'inventory-[0-9]+', but found %q", actual)
 	}
+	return assert.Equal(t, expected, actual)
+}
 
-	for tn, tc := range testCases {
-		t.Run(tn, func(t *testing.T) {
-			// Set up fake test factory
-			tf := cmdtesting.NewTestFactory().WithNamespace("test-namespace")
-			defer tf.Cleanup()
-			ioStreams, _, _, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled
-
-			// Set up temp directory with Ktpfile
-			dir, err := ioutil.TempDir("", "kpt-init-options-test")
-			assert.NoError(t, err)
-			defer os.RemoveAll(dir)
-			p := filepath.Join(dir, "Kptfile")
-			err = ioutil.WriteFile(p, []byte(tc.kptfile), 0600)
-			assert.NoError(t, err)
-			err = os.Chdir(dir)
-			assert.NoError(t, err)
-
-			// Create KptInitOptions and call Run()
-			cmd := NewCmdInit(tf, ioStreams)
-			cmd.SetArgs([]string{})
-			err = cmd.Execute()
-			assert.NoError(t, err)
-
-			// Otherwise, validate the kptfile values
-			assert.NoError(t, err)
-			kf, err := kptfileutil.ReadFile(dir)
-			assert.NoError(t, err)
-			assert.Equal(t, inventoryNamespace, kf.Inventory.Namespace)
-		})
+func assertGenInvID(t *testing.T, name, namespace, actual string) bool {
+	re := regexp.MustCompile(`^([a-z0-9]+)-[0-9]+$`)
+	match := re.FindStringSubmatch(actual)
+	if len(match) != 2 {
+		t.Errorf("unexpected format for autogenerated inventoryID")
+		return false
+	}
+	prefix, err := generateHash(namespace, name)
+	if err != nil {
+		panic(err)
+	}
+	if got, want := match[1], prefix; got != want {
+		t.Errorf("expected prefix %q, but found %q", want, got)
+		return false
 	}
+	return true
 }
diff --git a/commands/migratecmd_test.go b/internal/cmdmigrate/migratecmd_test.go
similarity index 94%
rename from commands/migratecmd_test.go
rename to internal/cmdmigrate/migratecmd_test.go
index ebca1e2e..09799b1d 100644
--- a/commands/migratecmd_test.go
+++ b/internal/cmdmigrate/migratecmd_test.go
@@ -1,7 +1,7 @@
 // Copyright 2020 Google LLC.
 // SPDX-License-Identifier: Apache-2.0
 
-package commands
+package cmdmigrate
 
 import (
 	"io/ioutil"
@@ -9,6 +9,7 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/GoogleContainerTools/kpt/internal/printer/fake"
 	"github.com/GoogleContainerTools/kpt/pkg/kptfile/kptfileutil"
 	"github.com/GoogleContainerTools/kpt/pkg/live"
 	"github.com/stretchr/testify/assert"
@@ -140,14 +141,15 @@ func TestKptMigrate_updateKptfile(t *testing.T) {
 			err = ioutil.WriteFile(p, []byte(tc.kptfile), 0600)
 			assert.NoError(t, err)
 
+			ctx := fake.CtxWithNilPrinter()
 			// Create MigrateRunner and call "updateKptfile"
 			cmProvider := provider.NewFakeProvider(tf, []object.ObjMetadata{})
 			rgProvider := live.NewResourceGroupProvider(tf)
 			cmLoader := manifestreader.NewManifestLoader(tf)
 			rgLoader := live.NewResourceGroupManifestLoader(tf)
-			migrateRunner := GetMigrateRunner(cmProvider, rgProvider, cmLoader, rgLoader, ioStreams)
+			migrateRunner := NewRunner(ctx, cmProvider, rgProvider, cmLoader, rgLoader, ioStreams)
 			migrateRunner.dryRun = tc.dryRun
-			err = migrateRunner.updateKptfile([]string{dir}, testInventoryID)
+			err = migrateRunner.updateKptfile(ctx, []string{dir}, testInventoryID)
 			// Check if there should be an error
 			if tc.isError {
 				if err == nil {
@@ -199,12 +201,13 @@ func TestKptMigrate_retrieveConfigMapInv(t *testing.T) {
 			defer tf.Cleanup()
 			ioStreams, _, _, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled
 
+			ctx := fake.CtxWithNilPrinter()
 			// Create MigrateRunner and call "retrieveConfigMapInv"
 			cmProvider := provider.NewFakeProvider(tf, []object.ObjMetadata{})
 			rgProvider := live.NewResourceGroupProvider(tf)
 			cmLoader := manifestreader.NewManifestLoader(tf)
 			rgLoader := live.NewResourceGroupManifestLoader(tf)
-			migrateRunner := GetMigrateRunner(cmProvider, rgProvider, cmLoader, rgLoader, ioStreams)
+			migrateRunner := NewRunner(ctx, cmProvider, rgProvider, cmLoader, rgLoader, ioStreams)
 			actual, err := migrateRunner.retrieveConfigMapInv(strings.NewReader(tc.configMap), []string{"-"})
 			// Check if there should be an error
 			if tc.isError {
@@ -302,12 +305,13 @@ func TestKptMigrate_migrateObjs(t *testing.T) {
 			defer tf.Cleanup()
 			ioStreams, _, _, _ := genericclioptions.NewTestIOStreams() //nolint:dogsled
 
+			ctx := fake.CtxWithNilPrinter()
 			// Create MigrateRunner and call "retrieveConfigMapInv"
 			cmProvider := provider.NewFakeProvider(tf, []object.ObjMetadata{})
 			rgProvider := live.NewFakeResourceGroupProvider(tf, tc.objs)
 			cmLoader := manifestreader.NewManifestLoader(tf)
 			rgLoader := live.NewResourceGroupManifestLoader(tf)
-			migrateRunner := GetMigrateRunner(cmProvider, rgProvider, cmLoader, rgLoader, ioStreams)
+			migrateRunner := NewRunner(ctx, cmProvider, rgProvider, cmLoader, rgLoader, ioStreams)
 			err := migrateRunner.migrateObjs(tc.objs, strings.NewReader(tc.invObj), []string{"-"})
 			// Check if there should be an error
 			if tc.isError {
diff --git a/internal/cmdrender/executor_test.go b/internal/cmdrender/executor_test.go
deleted file mode 100644
index f4fba4e7..00000000
--- a/internal/cmdrender/executor_test.go
+++ /dev/null
@@ -1,266 +0,0 @@
-// 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 cmdrender
-
-import (
-	"bytes"
-	"fmt"
-	"strings"
-	"testing"
-
-	"sigs.k8s.io/kustomize/kyaml/kio"
-)
-
-func TestDetectPathConflicts(t *testing.T) {
-	tests := map[string]struct {
-		input       string // input
-		expectedErr string // expected result
-	}{
-		"duplicate": {
-			input: `apiVersion: v1
-kind: Custom
-metadata:
-  name: a
-  annotations:
-    config.kubernetes.io/path: 'my/path/custom.yaml'
-    config.kubernetes.io/index: '0'
----
-apiVersion: v1
-kind: Custom
-metadata:
-  name: b
-  annotations:
-    config.kubernetes.io/path: 'my/path/custom.yaml'
-    config.kubernetes.io/index: '0'
-`,
-			expectedErr: `resource at path "my/path/custom.yaml" and index "0" already exists`,
-		},
-		"duplicate with `./` prefix": {
-			input: `apiVersion: v1
-kind: Custom
-metadata:
-  name: a
-  annotations:
-    config.kubernetes.io/path: 'my/path/custom.yaml'
-    config.kubernetes.io/index: '0'
----
-apiVersion: v1
-kind: Custom
-metadata:
-  name: b
-  annotations:
-    config.kubernetes.io/path: './my/path/custom.yaml'
-    config.kubernetes.io/index: '0'
-`,
-			expectedErr: `resource at path "my/path/custom.yaml" and index "0" already exists`,
-		},
-		"duplicate path, not index": {
-			input: `apiVersion: v1
-kind: Custom
-metadata:
-  name: a
-  annotations:
-    config.kubernetes.io/path: 'my/path/custom.yaml'
-    config.kubernetes.io/index: '0'
----
-apiVersion: v1
-kind: Custom
-metadata:
-  name: b
-  annotations:
-    config.kubernetes.io/path: 'my/path/custom.yaml'
-    config.kubernetes.io/index: '1'
-`,
-		},
-		"duplicate index, not path": {
-			input: `apiVersion: v1
-kind: Custom
-metadata:
-  name: a
-  annotations:
-    config.kubernetes.io/path: 'my/path/a.yaml'
-    config.kubernetes.io/index: '0'
----
-apiVersion: v1
-kind: Custom
-metadata:
-  name: b
-  annotations:
-    config.kubernetes.io/path: 'my/path/b.yaml'
-    config.kubernetes.io/index: '0'
-`,
-		},
-		"larger number of resources with duplicate": {
-			input: `apiVersion: v1
-kind: Custom
-metadata:
-  name: a
-  annotations:
-    config.kubernetes.io/path: 'my/path/a.yaml'
-    config.kubernetes.io/index: '0'
----
-apiVersion: v1
-kind: Custom
-metadata:
-  name: b
-  annotations:
-    config.kubernetes.io/path: 'my/path/a.yaml'
-    config.kubernetes.io/index: '1'
----
-apiVersion: v1
-kind: Custom
-metadata:
-  name: b
-  annotations:
-    config.kubernetes.io/path: 'my/path/b.yaml'
-    config.kubernetes.io/index: '0'
----
-apiVersion: v1
-kind: Custom
-metadata:
-  name: b
-  annotations:
-    config.kubernetes.io/path: 'my/path/b.yaml'
-    config.kubernetes.io/index: '1'
----
-apiVersion: v1
-kind: Custom
-metadata:
-  name: b
-  annotations:
-    config.kubernetes.io/path: 'my/path/b.yaml'
-    config.kubernetes.io/index: '2'
----
-apiVersion: v1
-kind: Custom
-metadata:
-  name: b
-  annotations:
-    config.kubernetes.io/path: 'my/path/c.yaml'
-    config.kubernetes.io/index: '0'
----
-apiVersion: v1
-kind: Custom
-metadata:
-  name: b
-  annotations:
-    config.kubernetes.io/path: 'my/path/c.yaml'
-    config.kubernetes.io/index: '1'
----
-apiVersion: v1
-kind: Custom
-metadata:
-  name: b
-  annotations:
-    config.kubernetes.io/path: 'my/path/b.yaml'
-    config.kubernetes.io/index: '1'
-`,
-			expectedErr: `resource at path "my/path/b.yaml" and index "1" already exists`,
-		},
-		"larger number of resources without duplicates": {
-			input: `apiVersion: v1
-kind: Custom
-metadata:
-  name: a
-  annotations:
-    config.kubernetes.io/path: 'my/path/a.yaml'
-    config.kubernetes.io/index: '0'
----
-apiVersion: v1
-kind: Custom
-metadata:
-  name: b
-  annotations:
-    config.kubernetes.io/path: 'my/path/a.yaml'
-    config.kubernetes.io/index: '1'
----
-apiVersion: v1
-kind: Custom
-metadata:
-  name: b
-  annotations:
-    config.kubernetes.io/path: 'my/path/b.yaml'
-    config.kubernetes.io/index: '0'
----
-apiVersion: v1
-kind: Custom
-metadata:
-  name: b
-  annotations:
-    config.kubernetes.io/path: 'my/path/b.yaml'
-    config.kubernetes.io/index: '1'
----
-apiVersion: v1
-kind: Custom
-metadata:
-  name: b
-  annotations:
-    config.kubernetes.io/path: 'my/path/b.yaml'
-    config.kubernetes.io/index: '2'
----
-apiVersion: v1
-kind: Custom
-metadata:
-  name: b
-  annotations:
-    config.kubernetes.io/path: 'my/path/c.yaml'
-    config.kubernetes.io/index: '0'
----
-apiVersion: v1
-kind: Custom
-metadata:
-  name: b
-  annotations:
-    config.kubernetes.io/path: 'my/path/c.yaml'
-    config.kubernetes.io/index: '1'
----
-apiVersion: v1
-kind: Custom
-metadata:
-  name: b
-  annotations:
-    config.kubernetes.io/path: 'my/path/b.yaml'
-    config.kubernetes.io/index: '3'
-`,
-		},
-	}
-	for _, tc := range tests {
-		out := &bytes.Buffer{}
-		r := kio.ByteReadWriter{
-			Reader:                bytes.NewBufferString(tc.input),
-			Writer:                out,
-			KeepReaderAnnotations: true,
-			OmitReaderAnnotations: true,
-		}
-		n, err := r.Read()
-		if err != nil {
-			fmt.Println("1")
-			t.FailNow()
-		}
-		err = detectPathConflicts(n)
-		if err != nil && tc.expectedErr == "" {
-			t.Errorf("unexpected error %s", err.Error())
-			t.FailNow()
-		}
-		if tc.expectedErr != "" && err == nil {
-			t.Errorf("expected error %s", tc.expectedErr)
-			t.FailNow()
-		}
-		if tc.expectedErr != "" && !strings.Contains(err.Error(), tc.expectedErr) {
-			t.Errorf("wanted error %s, got %s", tc.expectedErr, err.Error())
-			t.FailNow()
-		}
-	}
-}
diff --git a/internal/fnruntime/runner_test.go b/internal/fnruntime/runner_test.go
index 0fdc69e9..3ec3dd6f 100644
--- a/internal/fnruntime/runner_test.go
+++ b/internal/fnruntime/runner_test.go
@@ -17,14 +17,17 @@
 package fnruntime
 
 import (
+	"bytes"
 	"io/ioutil"
 	"os"
 	"path"
+	"strings"
 	"testing"
 
 	"github.com/GoogleContainerTools/kpt/internal/types"
 	kptfilev1alpha2 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1alpha2"
 	"github.com/stretchr/testify/assert"
+	"sigs.k8s.io/kustomize/kyaml/kio"
 	"sigs.k8s.io/kustomize/kyaml/yaml"
 )
 
@@ -163,3 +166,284 @@ func TestMultilineFormatter(t *testing.T) {
 		})
 	}
 }
+
+func TestEnforcePathInvariants(t *testing.T) {
+	tests := map[string]struct {
+		input       string // input
+		expectedErr string // expected result
+	}{
+		"duplicate": {
+			input: `apiVersion: v1
+kind: Custom
+metadata:
+  name: a
+  annotations:
+    config.kubernetes.io/path: 'my/path/custom.yaml'
+    config.kubernetes.io/index: '0'
+---
+apiVersion: v1
+kind: Custom
+metadata:
+  name: b
+  annotations:
+    config.kubernetes.io/path: 'my/path/custom.yaml'
+    config.kubernetes.io/index: '0'
+`,
+			expectedErr: `resource at path "my/path/custom.yaml" and index "0" already exists`,
+		},
+		"duplicate with `./` prefix": {
+			input: `apiVersion: v1
+kind: Custom
+metadata:
+  name: a
+  annotations:
+    config.kubernetes.io/path: 'my/path/custom.yaml'
+    config.kubernetes.io/index: '0'
+---
+apiVersion: v1
+kind: Custom
+metadata:
+  name: b
+  annotations:
+    config.kubernetes.io/path: './my/path/custom.yaml'
+    config.kubernetes.io/index: '0'
+`,
+			expectedErr: `resource at path "my/path/custom.yaml" and index "0" already exists`,
+		},
+		"duplicate path, not index": {
+			input: `apiVersion: v1
+kind: Custom
+metadata:
+  name: a
+  annotations:
+    config.kubernetes.io/path: 'my/path/custom.yaml'
+    config.kubernetes.io/index: '0'
+---
+apiVersion: v1
+kind: Custom
+metadata:
+  name: b
+  annotations:
+    config.kubernetes.io/path: 'my/path/custom.yaml'
+    config.kubernetes.io/index: '1'
+`,
+		},
+		"duplicate index, not path": {
+			input: `apiVersion: v1
+kind: Custom
+metadata:
+  name: a
+  annotations:
+    config.kubernetes.io/path: 'my/path/a.yaml'
+    config.kubernetes.io/index: '0'
+---
+apiVersion: v1
+kind: Custom
+metadata:
+  name: b
+  annotations:
+    config.kubernetes.io/path: 'my/path/b.yaml'
+    config.kubernetes.io/index: '0'
+`,
+		},
+		"larger number of resources with duplicate": {
+			input: `apiVersion: v1
+kind: Custom
+metadata:
+  name: a
+  annotations:
+    config.kubernetes.io/path: 'my/path/a.yaml'
+    config.kubernetes.io/index: '0'
+---
+apiVersion: v1
+kind: Custom
+metadata:
+  name: b
+  annotations:
+    config.kubernetes.io/path: 'my/path/a.yaml'
+    config.kubernetes.io/index: '1'
+---
+apiVersion: v1
+kind: Custom
+metadata:
+  name: b
+  annotations:
+    config.kubernetes.io/path: 'my/path/b.yaml'
+    config.kubernetes.io/index: '0'
+---
+apiVersion: v1
+kind: Custom
+metadata:
+  name: b
+  annotations:
+    config.kubernetes.io/path: 'my/path/b.yaml'
+    config.kubernetes.io/index: '1'
+---
+apiVersion: v1
+kind: Custom
+metadata:
+  name: b
+  annotations:
+    config.kubernetes.io/path: 'my/path/b.yaml'
+    config.kubernetes.io/index: '2'
+---
+apiVersion: v1
+kind: Custom
+metadata:
+  name: b
+  annotations:
+    config.kubernetes.io/path: 'my/path/c.yaml'
+    config.kubernetes.io/index: '0'
+---
+apiVersion: v1
+kind: Custom
+metadata:
+  name: b
+  annotations:
+    config.kubernetes.io/path: 'my/path/c.yaml'
+    config.kubernetes.io/index: '1'
+---
+apiVersion: v1
+kind: Custom
+metadata:
+  name: b
+  annotations:
+    config.kubernetes.io/path: 'my/path/b.yaml'
+    config.kubernetes.io/index: '1'
+`,
+			expectedErr: `resource at path "my/path/b.yaml" and index "1" already exists`,
+		},
+		"larger number of resources without duplicates": {
+			input: `apiVersion: v1
+kind: Custom
+metadata:
+  name: a
+  annotations:
+    config.kubernetes.io/path: 'my/path/a.yaml'
+    config.kubernetes.io/index: '0'
+---
+apiVersion: v1
+kind: Custom
+metadata:
+  name: b
+  annotations:
+    config.kubernetes.io/path: 'my/path/a.yaml'
+    config.kubernetes.io/index: '1'
+---
+apiVersion: v1
+kind: Custom
+metadata:
+  name: b
+  annotations:
+    config.kubernetes.io/path: 'my/path/b.yaml'
+    config.kubernetes.io/index: '0'
+---
+apiVersion: v1
+kind: Custom
+metadata:
+  name: b
+  annotations:
+    config.kubernetes.io/path: 'my/path/b.yaml'
+    config.kubernetes.io/index: '1'
+---
+apiVersion: v1
+kind: Custom
+metadata:
+  name: b
+  annotations:
+    config.kubernetes.io/path: 'my/path/b.yaml'
+    config.kubernetes.io/index: '2'
+---
+apiVersion: v1
+kind: Custom
+metadata:
+  name: b
+  annotations:
+    config.kubernetes.io/path: 'my/path/c.yaml'
+    config.kubernetes.io/index: '0'
+---
+apiVersion: v1
+kind: Custom
+metadata:
+  name: b
+  annotations:
+    config.kubernetes.io/path: 'my/path/c.yaml'
+    config.kubernetes.io/index: '1'
+---
+apiVersion: v1
+kind: Custom
+metadata:
+  name: b
+  annotations:
+    config.kubernetes.io/path: 'my/path/b.yaml'
+    config.kubernetes.io/index: '3'
+`,
+		},
+
+		"no error": {
+			input: `
+apiVersion: apps/v1
+kind: StatefulSet
+metadata:
+  name: my-stateful-set
+  annotations:
+    config.kubernetes.io/path: my-stateful-set.yaml
+spec:
+  replicas: 3
+`,
+		},
+		"with ../ prefix": {
+			input: `
+apiVersion: apps/v1
+kind: StatefulSet
+metadata:
+  name: my-stateful-set
+  annotations:
+    config.kubernetes.io/path: ../my-stateful-set.yaml
+spec:
+  replicas: 3
+
+`,
+			expectedErr: "function must not modify resources outside of package: resource has path ../my-stateful-set.yaml",
+		},
+		"with nested ../ in path": {
+			input: `
+apiVersion: apps/v1
+kind: StatefulSet
+metadata:
+  name: my-stateful-set
+  annotations:
+    config.kubernetes.io/path: a/b/../../../my-stateful-set.yaml
+spec:
+  replicas: 3
+`,
+			expectedErr: "function must not modify resources outside of package: resource has path a/b/../../../my-stateful-set.yaml",
+		},
+	}
+	for _, tc := range tests {
+		out := &bytes.Buffer{}
+		r := kio.ByteReadWriter{
+			Reader:                bytes.NewBufferString(tc.input),
+			Writer:                out,
+			KeepReaderAnnotations: true,
+			OmitReaderAnnotations: true,
+		}
+		n, err := r.Read()
+		if err != nil {
+			t.FailNow()
+		}
+		err = enforcePathInvariants(n)
+		if err != nil && tc.expectedErr == "" {
+			t.Errorf("unexpected error %s", err.Error())
+			t.FailNow()
+		}
+		if tc.expectedErr != "" && err == nil {
+			t.Errorf("expected error %s", tc.expectedErr)
+			t.FailNow()
+		}
+		if tc.expectedErr != "" && !strings.Contains(err.Error(), tc.expectedErr) {
+			t.Errorf("wanted error %s, got %s", tc.expectedErr, err.Error())
+			t.FailNow()
+		}
+	}
+}
diff --git a/internal/util/addmergecomment/addmergecomment_test.go b/internal/util/addmergecomment/addmergecomment_test.go
index 94858297..14662e41 100644
--- a/internal/util/addmergecomment/addmergecomment_test.go
+++ b/internal/util/addmergecomment/addmergecomment_test.go
@@ -1,3 +1,17 @@
+// 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 addmergecomment
 
 import (
diff --git a/internal/util/diff/diff_test.go b/internal/util/diff/diff_test.go
index 2285e0df..bda4acc4 100644
--- a/internal/util/diff/diff_test.go
+++ b/internal/util/diff/diff_test.go
@@ -346,7 +346,7 @@ locally changed: foo
 
 			filteredOutput := filterDiffMetadata(diffOutput)
 			if tc.hasLocalSubpackageChanges {
-				filteredOutput = regexp.MustCompile("Only in /tmp.+:").ReplaceAllString(filteredOutput, "locally changed:")
+				filteredOutput = regexp.MustCompile("Only in /(tmp|var).+:").ReplaceAllString(filteredOutput, "locally changed:")
 			}
 			assert.Equal(t, strings.TrimSpace(tc.expDiff)+"\n", filteredOutput)
 		})
diff --git a/internal/util/diff/pkgdiff_test.go b/internal/util/diff/pkgdiff_test.go
index 2a033bfc..a774c188 100644
--- a/internal/util/diff/pkgdiff_test.go
+++ b/internal/util/diff/pkgdiff_test.go
@@ -75,7 +75,7 @@ func TestPkgDiff(t *testing.T) {
 			diff: toStringSet("Kptfile"),
 		},
 		{
-			name: "subpackages are not included",
+			name: "subpackages are not included in no diff",
 			pkg1: pkgbuilder.NewRootPkg().
 				WithKptfile().
 				WithResource(pkgbuilder.DeploymentResource).
@@ -94,6 +94,46 @@ func TestPkgDiff(t *testing.T) {
 				),
 			diff: toStringSet(),
 		},
+		{
+			name: "subpackages are not included in root package diff",
+			pkg1: pkgbuilder.NewRootPkg().
+				WithKptfile().
+				WithResource(pkgbuilder.ConfigMapResource).
+				WithSubPackages(
+					pkgbuilder.NewSubPkg("subpackage").
+						WithKptfile(pkgbuilder.NewKptfile()).
+						WithResource(pkgbuilder.DeploymentResource),
+				),
+			pkg2: pkgbuilder.NewRootPkg().
+				WithKptfile().
+				WithResource(pkgbuilder.DeploymentResource).
+				WithSubPackages(
+					pkgbuilder.NewSubPkg("subpackage").
+						WithKptfile().
+						WithResource(pkgbuilder.ConfigMapResource),
+				),
+			diff: toStringSet("deployment.yaml", "configmap.yaml"),
+		},
+		{
+			name: "package doesn't exist",
+			pkg1: nil,
+			pkg2: pkgbuilder.NewRootPkg().
+				WithKptfile(pkgbuilder.NewKptfile()),
+			diff: toStringSet("Kptfile"),
+		},
+		{
+			name: "package doesn't have a kptfile",
+			pkg1: pkgbuilder.NewRootPkg(),
+			pkg2: pkgbuilder.NewRootPkg().
+				WithKptfile().
+				WithResource(pkgbuilder.DeploymentResource).
+				WithSubPackages(
+					pkgbuilder.NewSubPkg("subpackage").
+						WithKptfile().
+						WithResource(pkgbuilder.ConfigMapResource),
+				),
+			diff: toStringSet("Kptfile", "deployment.yaml"),
+		},
 	}
 
 	for i := range testCases {
diff --git a/pkg/test/runner/README.md b/pkg/test/runner/README.md
new file mode 100644
index 00000000..ed071ca1
--- /dev/null
+++ b/pkg/test/runner/README.md
@@ -0,0 +1,68 @@
+This package is an end-to-end test runner for kpt fn commands. It
+can also be used to test kpt KRM functions.
+
+# How Does It Work
+
+After importing this package, you can use `runner.ScanTestCases` to
+recursively scan the test cases in a given directory. For each test
+case, call `runner.NewRunner` to create a test runner and then call
+`Run` to run the test. For example please see `e2e/fn_test.go`
+
+All test cases should be **independent** and should be able to run in
+fully parallel. They should be **self-contained** so the runner doesn't
+need any manual configuration.
+
+The configurations for a test case is located in the `.expected` directory
+in the test case directory. The runner will implicitly use following files
+in `.expected`:
+
+- `config.yaml`: This is the file which contains the configurations for the
+  test case. It can have following fields:
+  - `testType`: `eval` or `render`. It controls which `kpt fn` command will be
+    used to run the test. Default: `eval`.
+  - `exitCode`: The expected exit code for the command. Default: 0.
+  - `skip`: Runner will skip the test if `skip` is set to true. Default: false.
+  - `notIdempotent`: The functions and commands should be idempotent, but in
+    some cases it's not doable. The runner will not run the command twice to
+    check idempotent if this is set to true. Default: false.
+  - `debug`: Debug means will the debug behavior be enabled. Default: false.
+    Debug behaviors:
+    - Keep the temporary directory used to run the test cases after test.
+  - `stdOut`: The expected standard output from running the command. Default: "".
+  - `stdErr`: The expected standard error from running the command. Default: "".
+  - `disableOutputTruncate`: Control should error output be truncated. Default:
+    false.
+  - Configurations only apply to `eval` tests:
+    - `execPath`: A path to the executable file that will be run as function.
+      Mutually exclusive with Image. The path should be separated by slash '/'
+    - `image`: The image name for the function. Either `image` or `execPath`
+      must be provided.
+    - `args`: A map contains the arguments that will be passed into function.
+      Args will be passed as 'key=value' format after the '--' in command.
+    - `network`: Control the accessibility of network from the function
+      container. Default: false.
+    - `includeMetaResources`: Enable including meta resources, like Kptfile,
+      in the function input. Default: false.
+    - `fnConfig`: The path to the function config file. The path should be
+      separated by slash '/'.
+- `diff.patch`: The expected `git diff` output after running the command.
+  Default: "".
+- `results.yaml`: The expected result file after running the command.
+  Default: "".
+- `setup.sh`: A **bash** script which will be run before the command if it exists.
+- `exec.sh`: A **bash** script which will be run if it exists and will replace the
+  command (`kpt fn eval` or `kpt fn render`) that will be run according to
+  `testType` in configurations. All configurations that used to control command
+  behavior, like `disableOutputTruncate` and `args`, will be ignored.
+- `teardown.sh`: A **bash** script which will be run after the command and
+  result comparison if it exists.
+
+# Expected Output
+
+The runner will compare the actual values of:
+
+- `stdout`
+- `stderr`
+- Exit Code
+- Diff
+- Results file
diff --git a/pkg/test/runner/runner.go b/pkg/test/runner/runner.go
index 59e7308d..03d760ef 100644
--- a/pkg/test/runner/runner.go
+++ b/pkg/test/runner/runner.go
@@ -35,10 +35,11 @@ type Runner struct {
 }
 
 func getKptBin() (string, error) {
-	if p := os.Getenv(kptBinEnv); p != "" {
-		return p, nil
+	p, err := exec.Command("which", "kpt").CombinedOutput()
+	if err != nil {
+		return "", fmt.Errorf("cannot find command 'kpt' in $PATH: %w", err)
 	}
-	return "", fmt.Errorf("must specify env '%s' for kpt binary path", kptBinEnv)
+	return strings.TrimSpace(string(p)), nil
 }
 
 const (
@@ -46,12 +47,14 @@ const (
 	// expected diff and results if they already exist. If will not change
 	// config.yaml.
 	updateExpectedEnv string = "KPT_E2E_UPDATE_EXPECTED"
-	kptBinEnv         string = "KPT_E2E_BIN"
 
 	expectedDir         string = ".expected"
 	expectedResultsFile string = "results.yaml"
 	expectedDiffFile    string = "diff.patch"
 	expectedConfigFile  string = "config.yaml"
+	setupScript         string = "setup.sh"
+	teardownScript      string = "teardown.sh"
+	execScript          string = "exec.sh"
 	CommandFnEval       string = "eval"
 	CommandFnRender     string = "render"
 )
@@ -91,6 +94,40 @@ func (r *Runner) Run() error {
 	}
 }
 
+// runSetupScript runs the setup script if the test has it
+func (r *Runner) runSetupScript(pkgPath string) error {
+	p, err := filepath.Abs(filepath.Join(r.testCase.Path, expectedDir, setupScript))
+	if err != nil {
+		return err
+	}
+	if _, err := os.Stat(p); os.IsNotExist(err) {
+		return nil
+	}
+	cmd := getCommand(pkgPath, "bash", []string{p})
+	r.t.Logf("running setup script: %q", cmd.String())
+	if output, err := cmd.CombinedOutput(); err != nil {
+		return fmt.Errorf("failed to run setup script %q.\nOutput: %q\n: %w", p, string(output), err)
+	}
+	return nil
+}
+
+// runTearDownScript runs the teardown script if the test has it
+func (r *Runner) runTearDownScript(pkgPath string) error {
+	p, err := filepath.Abs(filepath.Join(r.testCase.Path, expectedDir, teardownScript))
+	if err != nil {
+		return err
+	}
+	if _, err := os.Stat(p); os.IsNotExist(err) {
+		return nil
+	}
+	cmd := getCommand(pkgPath, "bash", []string{p})
+	r.t.Logf("running teardown script: %q", cmd.String())
+	if output, err := cmd.CombinedOutput(); err != nil {
+		return fmt.Errorf("failed to run teardown script %q.\nOutput: %q\n: %w", p, string(output), err)
+	}
+	return nil
+}
+
 func (r *Runner) runFnEval() error {
 	r.t.Logf("Running test against package %s\n", r.pkgName)
 	tmpDir, err := ioutil.TempDir("", "kpt-fn-e2e-*")
@@ -124,34 +161,51 @@ func (r *Runner) runFnEval() error {
 	}
 
 	// run function
-	kptArgs := []string{"fn", "eval", pkgPath}
+	for i := 0; i < r.testCase.Config.RunCount(); i++ {
+		err = r.runSetupScript(pkgPath)
+		if err != nil {
+			return err
+		}
 
-	if resultsDir != "" {
-		kptArgs = append(kptArgs, "--results-dir", resultsDir)
-	}
-	if r.testCase.Config.EvalConfig.Network {
-		kptArgs = append(kptArgs, "--network")
-	}
-	if r.testCase.Config.EvalConfig.Image != "" {
-		kptArgs = append(kptArgs, "--image", r.testCase.Config.EvalConfig.Image)
-	} else if !r.testCase.Config.EvalConfig.execUniquePath.Empty() {
-		kptArgs = append(kptArgs, "--exec-path", string(r.testCase.Config.EvalConfig.execUniquePath))
-	}
-	if !r.testCase.Config.EvalConfig.fnConfigUniquePath.Empty() {
-		kptArgs = append(kptArgs, "--fn-config", string(r.testCase.Config.EvalConfig.fnConfigUniquePath))
-	}
-	if r.testCase.Config.EvalConfig.IncludeMetaResources {
-		kptArgs = append(kptArgs, "--include-meta-resources")
-	}
-	// args must be appended last
-	if len(r.testCase.Config.EvalConfig.Args) > 0 {
-		kptArgs = append(kptArgs, "--")
-		for k, v := range r.testCase.Config.EvalConfig.Args {
-			kptArgs = append(kptArgs, fmt.Sprintf("%s=%s", k, v))
+		var cmd *exec.Cmd
+		execScriptPath, err := filepath.Abs(filepath.Join(r.testCase.Path, expectedDir, execScript))
+		if err != nil {
+			return err
 		}
-	}
-	for i := 0; i < r.testCase.Config.RunCount(); i++ {
-		stdout, stderr, fnErr := runCommand("", r.kptBin, kptArgs)
+
+		if _, err := os.Stat(execScriptPath); err == nil {
+			cmd = getCommand(pkgPath, "bash", []string{execScriptPath})
+		} else {
+			kptArgs := []string{"fn", "eval", pkgPath}
+
+			if resultsDir != "" {
+				kptArgs = append(kptArgs, "--results-dir", resultsDir)
+			}
+			if r.testCase.Config.EvalConfig.Network {
+				kptArgs = append(kptArgs, "--network")
+			}
+			if r.testCase.Config.EvalConfig.Image != "" {
+				kptArgs = append(kptArgs, "--image", r.testCase.Config.EvalConfig.Image)
+			} else if !r.testCase.Config.EvalConfig.execUniquePath.Empty() {
+				kptArgs = append(kptArgs, "--exec-path", string(r.testCase.Config.EvalConfig.execUniquePath))
+			}
+			if !r.testCase.Config.EvalConfig.fnConfigUniquePath.Empty() {
+				kptArgs = append(kptArgs, "--fn-config", string(r.testCase.Config.EvalConfig.fnConfigUniquePath))
+			}
+			if r.testCase.Config.EvalConfig.IncludeMetaResources {
+				kptArgs = append(kptArgs, "--include-meta-resources")
+			}
+			// args must be appended last
+			if len(r.testCase.Config.EvalConfig.Args) > 0 {
+				kptArgs = append(kptArgs, "--")
+				for k, v := range r.testCase.Config.EvalConfig.Args {
+					kptArgs = append(kptArgs, fmt.Sprintf("%s=%s", k, v))
+				}
+			}
+			cmd = getCommand("", r.kptBin, kptArgs)
+		}
+		r.t.Logf("running command %q", cmd.String())
+		stdout, stderr, fnErr := runCommand(cmd)
 		if fnErr != nil {
 			r.t.Logf("kpt error, stdout: %s; stderr: %s", stdout, stderr)
 		}
@@ -170,6 +224,11 @@ func (r *Runner) runFnEval() error {
 		if fnErr != nil {
 			break
 		}
+
+		err = r.runTearDownScript(pkgPath)
+		if err != nil {
+			return err
+		}
 	}
 
 	return nil
@@ -230,17 +289,35 @@ func (r *Runner) runFnRender() error {
 	}
 
 	// run function
-	kptArgs := []string{"fn", "render", pkgPath}
+	for i := 0; i < r.testCase.Config.RunCount(); i++ {
+		err = r.runSetupScript(pkgPath)
+		if err != nil {
+			return err
+		}
 
-	if resultsDir != "" {
-		kptArgs = append(kptArgs, "--results-dir", resultsDir)
-	}
+		var cmd *exec.Cmd
 
-	if r.testCase.Config.DisableOutputTruncate {
-		kptArgs = append(kptArgs, "--truncate-output=false")
-	}
-	for i := 0; i < r.testCase.Config.RunCount(); i++ {
-		stdout, stderr, fnErr := runCommand("", r.kptBin, kptArgs)
+		execScriptPath, err := filepath.Abs(filepath.Join(r.testCase.Path, expectedDir, execScript))
+		if err != nil {
+			return err
+		}
+
+		if _, err := os.Stat(execScriptPath); err == nil {
+			cmd = getCommand(pkgPath, "bash", []string{execScriptPath})
+		} else {
+			kptArgs := []string{"fn", "render", pkgPath}
+
+			if resultsDir != "" {
+				kptArgs = append(kptArgs, "--results-dir", resultsDir)
+			}
+
+			if r.testCase.Config.DisableOutputTruncate {
+				kptArgs = append(kptArgs, "--truncate-output=false")
+			}
+			cmd = getCommand("", r.kptBin, kptArgs)
+		}
+		r.t.Logf("running command %q", cmd.String())
+		stdout, stderr, fnErr := runCommand(cmd)
 		// Update the diff file or results file if updateExpectedEnv is set.
 		if strings.ToLower(os.Getenv(updateExpectedEnv)) == "true" {
 			return r.updateExpected(pkgPath, resultsDir, filepath.Join(r.testCase.Path, expectedDir))
@@ -259,6 +336,11 @@ func (r *Runner) runFnRender() error {
 		if fnErr != nil {
 			break
 		}
+
+		err = r.runTearDownScript(pkgPath)
+		if err != nil {
+			return err
+		}
 	}
 	return nil
 }
diff --git a/pkg/test/runner/util.go b/pkg/test/runner/util.go
index 288f98bb..2e94d29e 100644
--- a/pkg/test/runner/util.go
+++ b/pkg/test/runner/util.go
@@ -23,9 +23,7 @@ import (
 	"path/filepath"
 )
 
-func runCommand(pwd, name string, arg []string) (string, string, error) {
-	cmd := exec.Command(name, arg...)
-	cmd.Dir = pwd
+func runCommand(cmd *exec.Cmd) (string, string, error) {
 	var stdout bytes.Buffer
 	var stderr bytes.Buffer
 	cmd.Stdout = &stdout
@@ -34,13 +32,19 @@ func runCommand(pwd, name string, arg []string) (string, string, error) {
 	return stdout.String(), stderr.String(), err
 }
 
+func getCommand(pwd, name string, arg []string) *exec.Cmd {
+	cmd := exec.Command(name, arg...)
+	cmd.Dir = pwd
+	return cmd
+}
+
 func copyDir(src, dst string) error {
-	_, _, err := runCommand("", "cp", []string{"-r", src, dst})
+	_, _, err := runCommand(getCommand("", "cp", []string{"-r", src, dst}))
 	return err
 }
 
 func gitInit(d string) error {
-	stdout, stderr, err := runCommand(d, "git", []string{"init"})
+	stdout, stderr, err := runCommand(getCommand(d, "git", []string{"init"}))
 	if err != nil {
 		return fmt.Errorf("git init error: %w, output: %s, stderr: %s", err, stdout, stderr)
 	}
@@ -48,7 +52,7 @@ func gitInit(d string) error {
 }
 
 func gitAddAll(d string) error {
-	stdout, stderr, err := runCommand(d, "git", []string{"add", "--all"})
+	stdout, stderr, err := runCommand(getCommand(d, "git", []string{"add", "--all"}))
 	if err != nil {
 		return fmt.Errorf("git commit error: %w, output: %s, stderr: %s", err, stdout, stderr)
 	}
@@ -56,15 +60,15 @@ func gitAddAll(d string) error {
 }
 
 func gitCommit(d, msg string) error {
-	stdout, stderr, err := runCommand(d, "git", []string{"config", "user.name", "none"})
+	stdout, stderr, err := runCommand(getCommand(d, "git", []string{"config", "user.name", "none"}))
 	if err != nil {
 		return fmt.Errorf("git config error: %w, output: %s, stderr: %s", err, stdout, stderr)
 	}
-	stdout, stderr, err = runCommand(d, "git", []string{"config", "user.email", "none"})
+	stdout, stderr, err = runCommand(getCommand(d, "git", []string{"config", "user.email", "none"}))
 	if err != nil {
 		return fmt.Errorf("git config error: %w, output: %s, stderr: %s", err, stdout, stderr)
 	}
-	stdout, stderr, err = runCommand(d, "git", []string{"commit", "-m", msg, "--allow-empty"})
+	stdout, stderr, err = runCommand(getCommand(d, "git", []string{"commit", "-m", msg, "--allow-empty"}))
 	if err != nil {
 		return fmt.Errorf("git commit error: %w, output: %s, stderr: %s", err, stdout, stderr)
 	}
@@ -72,7 +76,7 @@ func gitCommit(d, msg string) error {
 }
 
 func gitDiff(d, commit1, commit2 string) (string, error) {
-	stdout, stderr, err := runCommand(d, "git", []string{"diff", commit1, commit2})
+	stdout, stderr, err := runCommand(getCommand(d, "git", []string{"diff", commit1, commit2}))
 	if err != nil {
 		return "", fmt.Errorf("git diff error: %w, output: %s, stderr: %s", err, stdout, stderr)
 	}
@@ -80,7 +84,7 @@ func gitDiff(d, commit1, commit2 string) (string, error) {
 }
 
 func getCommitHash(d string) (string, error) {
-	stdout, stderr, err := runCommand(d, "git", []string{"log", "-n", "1", "--pretty=format:%h"})
+	stdout, stderr, err := runCommand(getCommand(d, "git", []string{"log", "-n", "1", "--pretty=format:%h"}))
 	if err != nil {
 		return "", fmt.Errorf("git log error: %w, output: %s, stderr: %s", err, stdout, stderr)
 	}
@@ -102,6 +106,6 @@ func diffStrings(actual, expected string) (string, error) {
 		return "", fmt.Errorf("failed to write file %s", expectedPath)
 	}
 	// diff is expected to exit with 1 so we ignore the error here
-	output, _, _ := runCommand(tmpDir, "diff", []string{"-u", expectedPath, actualPath})
+	output, _, _ := runCommand(getCommand(tmpDir, "diff", []string{"-u", expectedPath, actualPath}))
 	return output, nil
 }