Repository: GoogleContainerTools/kpt
Pull request #2057 (2021-05-19 23:35:11)

book: chapter 5 part 1
Add documentation for TS and Go SDK.
Also add style guides for contributors.

Part of #1573


Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index c980eaf2..eb40cccc 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -26,6 +26,13 @@ information on using pull requests.
 This project follows [Google's Open Source Community Guidelines] and a [Code of
 Conduct].
 
+## Style Guides
+
+Contributions are required to follow these style guides:
+
+- [Error Message Style Guide]
+- [Documentation Style Guide]
+
 ## Building the Source
 
 1. Clone the project
@@ -47,15 +54,21 @@ Conduct].
    make all
    ```
 
-## Contributing to docs
+## Contribution to the Functions Catalog
+
+Refer to the
+[documentation in the kpt-functions-catalog](https://github.com/GoogleContainerTools/kpt-functions-catalog/blob/master/CONTRIBUTING.md)
+repo.
+
+## Contributing to Documentation
 
-If you are updating the documentation, please do it in separate PRs from
-code changes and the commit message should start with `Docs:`.
+If you are updating the documentation, please do it in separate PRs from code
+changes and PR description should start with `Docs:`.
 
 ### Run the docs locally
 
-Make docs changes and test them by running the site in a docker container
-with `make site-run-server`.
+Make docs changes and test them by running the site in a docker container with
+`make site-run-server`.
 
 It's usually a good idea to test locally for the following:
 
@@ -67,25 +80,6 @@ It's usually a good idea to test locally for the following:
 Docs are under [site/] and use [docsify] to present the source markdown files.
 The sidebar is automatically updated for the site at deployment time.
 
-### Adding or updating catalog functions
-
-<!-- TODO: Update this once the catalog is live. -->
-The config functions catalog is auto-generated using [this node package]. Update
-the catalog.json file with the appropriate information for your function like
-its type (source, sink, validator, transformer, or generator). Regenerate the
-catalog page with:
-
-`make gencatalog`
-
-### Adding or updating asciinema
-
-- asciinema casts are under `site/static/casts`
-- add or modify a `*.sh*` script which will run the commands that will be
-  recorded
-- run `make-gif.sh` with the script name (without extension) as the argument
-- add the updated cast to git
-- Reference the cast using the `asciinema` shortcode
-
 ### Docs Hygiene
 
 The kpt website uses markdownlint to lint docs for formatting and style. Use
@@ -99,6 +93,8 @@ This includes:
 - Run the kpt website through the [W3 Link Checker] in recursive mode and fix
   warnings and errors.
 
+[error message style guide]: docs/style-guides/errors.md
+[documentation style guide]: docs/style-guides/docs.md
 [github help]: https://help.github.com/articles/about-pull-requests/
 [google's open source community guidelines]:
   https://opensource.google.com/conduct/
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 700fd26f..5123bd9c 100644
--- a/commands/livecmd.go
+++ b/commands/livecmd.go
@@ -21,12 +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"
@@ -92,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
diff --git a/docs/style-guides/docs.md b/docs/style-guides/docs.md
new file mode 100644
index 00000000..cb69bd26
--- /dev/null
+++ b/docs/style-guides/docs.md
@@ -0,0 +1,42 @@
+# kpt Docs Style Guide
+
+1. CLI commands name should appear with inline quotes everywhere including in
+   page titles:
+   - Correct: `diff`
+   - Incorrect: `Diff`
+   - Incorrect: `The Diff tool`
+2. Shell commands should be in `shell` code blocks and start with `$` to
+   distinguish the command from its output:
+
+   ```shell
+   $ kpt <command>
+   <output>
+   ```
+
+3. Links to other kpt doc pages should be absolute:
+   - Correct: `[pkg]: /reference/pkg/get/`
+   - Incorrect: `[pkg]: ../../../reference/pkg/get`
+4. Flags must appear after positional args:
+
+   - Correct:
+
+   ```shell
+   $ kpt fn eval my-package --image gcr.io/kpt-fn/search-replace
+   ```
+
+   - Incorrect:
+
+   ```shell
+   $ kpt fn eval --image gcr.io/kpt-fn/search-replace my-package
+   ```
+
+5. The name of the tool should always appear as small caps (even at start of
+   sentences) and not in block quotes:
+   - Correct: kpt
+   - Incorrect: `kpt`
+   - Incorrect: Kpt
+   - Incorrect: KPT
+6. References to a particular KRM group, version, kind, field should appear with
+   inline quotes:
+   - Correct: `ConfigMap`
+   - Incorrect: ConfigMap
diff --git a/docs/style-guides/errors.md b/docs/style-guides/errors.md
new file mode 100644
index 00000000..8696156f
--- /dev/null
+++ b/docs/style-guides/errors.md
@@ -0,0 +1,38 @@
+# kpt Error Messages Style Guide
+
+Validation error messages and documentation are an important part of the UX in
+kpt. In fact, validation error messages are most likely the first thing users
+experience. The general philosophy here is to have **precise**, **actionable**,
+and **consistent** error messages.
+
+## Errors vs. Documentation
+
+In most cases, the error message should provide enough information to resolve
+the issue. Prefer short descriptions that explain what caused the specific error
+and what to do to fix it. More extensive explanations and general rules (if they
+take more than a sentence to describe) are for documentation (which is
+automatically linked in error messages).
+
+Generally, documentation should expand on what the error message says and help
+the reader understand how to prevent future mistakes.
+
+## Error Message Rules
+
+In the table below, origin and scope of rules are denoted with a prefix
+
+- **K** is inherited from
+  [Kubernetes conventions](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#validation);
+  rules with prefix
+- **R** are specific to Kpt.
+
+<!-- prettier-ignore -->
+|  Rule  | Description  | Examples  |
+| --- | --- | --- |
+| K1  | Where possible, tell users what they CAN instead of what they CANNOT do.  |   |
+| K2  | When asserting a requirement in the positive, use "must". Avoid words like "should" as they imply that the assertion is optional.  | a) "must be greater than 0"  b) "must match regex '[a-z]+'"  |
+| K3  | When asserting a formatting requirement in the negative, use "must not". Avoid words like "should not" as they imply that the assertion is optional.  | "must not contain '..'"  |
+| K5  | When referencing a user-provided string value, indicate the literal in quotes.  N Use quotes (%q format specifier in Go) around user-provided values. This includes file paths.  | "must not contain '..'"  |
+| K6  | When referencing a field name, indicate the name in back-quotes.  N Where unclear from the message, reference the full field path.  | "must be greater than `spec.request.size`"  |
+| K7  | When specifying inequalities, use words rather than symbols. Do not use words like "larger than", "bigger than", "more than", "higher than", etc.  | a) "must be less than 256"  b) "must be greater than or equal to 0".   |
+| K8  | When specifying numeric ranges, use inclusive ranges when possible.  |   |
+| R1  | If wrapping a runtime error, such as the result of a failed API Server call, use %w formatting verb in fmt.Errorf()(err) to include the root cause error. Refer to [Go error enhancements in 1.13](https://blog.golang.org/go1.13-errors).  |   |
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/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/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/pkgutil/pkgutil.go b/internal/util/pkgutil/pkgutil.go
index 09a1d34c..ed4f4e4a 100644
--- a/internal/util/pkgutil/pkgutil.go
+++ b/internal/util/pkgutil/pkgutil.go
@@ -24,6 +24,7 @@ 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"
@@ -293,6 +294,43 @@ func FormatPackage(pkgPath string) {
 		// 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,
diff --git a/site/book/05-developing-functions/00.md b/site/book/05-developing-functions/00.md
index 1333ed77..89c1053a 100644
--- a/site/book/05-developing-functions/00.md
+++ b/site/book/05-developing-functions/00.md
@@ -1,1 +1,12 @@
-TODO
+[Chapter 2][ch2] provided a high-level conceptual explanation of functions. We
+discussed how this architecture enables us to develop functions in different
+toolchains, languages and runtimes. this chapter, we are going to look at
+several different approaches to developing functions.
+
+We are also going to take a closer look at the standardized API between the
+orchestrator (i.e. kpt CLI) and the function containers. Understanding this API
+enables you to have a deeper understanding of how things work under the hood. It
+also enables you to create your own toolchain for function development if you so
+desire.
+
+[ch2]: /book/02-concepts/03-functions
diff --git a/site/book/05-developing-functions/03-developing-in-Go.md b/site/book/05-developing-functions/03-developing-in-Go.md
index 1333ed77..43ec0ff1 100644
--- a/site/book/05-developing-functions/03-developing-in-Go.md
+++ b/site/book/05-developing-functions/03-developing-in-Go.md
@@ -1,1 +1,123 @@
-TODO
+You can use the `kyaml` library to development functions in Go. It provides the
+following features:
+
+- **General-purpose language:** Compared to Domain Specific Languages (DSL), Go
+  is a general-purpose programming language that provides:
+  - Proper abstractions and language features
+  - A extensive ecosystem of tooling (e.g. IDE support)
+  - A comprehensive catalog of well-supported libraries
+  - Robust community support and detailed documentation
+- **YAML-centric**: As opposed to other frameworks discussed in this chapter,
+  the `kyaml` library exposes the YAML Abstract Syntax Tree (AST) to the user.
+  This enables you to control every aspect of the YAML file including
+  manipulating comments, but comes at the cost of complexity compared to
+  representing resources as idiomatic data structures.
+
+## Quickstart
+
+### Create the go module
+
+```shell
+$ go mod init github.com/user/repo
+$ go get sigs.k8s.io/kustomize/kyaml@v0.10.6
+```
+
+### Create the `main.go`
+
+This is a simple function that adds the annotation `myannotation` with the
+provided value:
+
+```go
+// main.go
+package main
+
+import (
+	"os"
+
+	"sigs.k8s.io/kustomize/kyaml/fn/framework"
+	"sigs.k8s.io/kustomize/kyaml/yaml"
+)
+
+var value string
+
+func main() {
+	resourceList := &framework.ResourceList{}
+	cmd := framework.Command(resourceList, func() error {
+		// cmd.Execute() will parse the ResourceList.functionConfig into
+		// cmd.Flags from the ResourceList.functionConfig.data field.
+		for i := range resourceList.Items {
+			// modify the resources using the kyaml/yaml library:
+			if err := resourceList.Items[i].PipeE(yaml.SetAnnotation("myannotation", value)); err != nil {
+				return err
+			}
+		}
+		return nil
+	})
+	cmd.Flags().StringVar(&value, "value", "", "annotation value")
+	if err := cmd.Execute(); err != nil {
+		os.Exit(1)
+	}
+}
+```
+
+### Build and test the function
+
+Build the go binary:
+
+```shell
+$ go build -o my-fn .
+```
+
+Test it by running imperatively:
+
+```shell
+$ kpt pkg get https://github.com/GoogleContainerTools/kpt.git/package-examples/wordpress@v0.3
+# run the my-fn function against the configuration in wordpress
+$ kpt fn eval wordpress --exec-path ./my-fn -- value=foo
+```
+
+During iterative development, `--exec-path` flag can be used to execute the
+function binary directly instead of requiring the function to be containerized
+first. Once you have a function binary that works, you can then proceed to
+creating the container image.
+
+### Publish the function
+
+Build the function into a container image:
+
+```shell
+# optional: generate a Dockerfile to contain the function
+$ go run ./main.go gen ./
+```
+
+```shell
+# build the function into an image
+$ docker build . -t gcr.io/project/fn-name:tag
+# optional: push the image to a container registry
+$ docker push gcr.io/project/fn-name:tag
+```
+
+Run the function imperatively as a container function:
+
+```shell
+$ kpt fn eval wordpress --image gcr.io/project/fn-name:tag -- value=foo
+```
+
+## Next Steps
+
+- Read the package documentation:
+
+| Package                                    | Purpose               |
+| ------------------------------------------ | --------------------- |
+| [sigs.k8s.io/kustomize/kyaml/fn/framework] | Functions Framework   |
+| [sigs.k8s.io/kustomize/kyaml/yaml]         | Modify YAML resources |
+
+- Take a look at the source code for [functions in the catalog] to better
+  understand how to use the develop functions in Go
+
+[sigs.k8s.io/kustomize/kyaml/fn/framework]:
+  https://pkg.go.dev/sigs.k8s.io/kustomize/kyaml@v0.10.16/fn/framework#pkg-index
+[sigs.k8s.io/kustomize/kyaml/yaml]:
+  https://pkg.go.dev/sigs.k8s.io/kustomize/kyaml@v0.10.16/yaml
+[functions in the catalog]:
+  https://github.com/GoogleContainerTools/kpt-functions-catalog/tree/master/functions/go
diff --git a/site/book/05-developing-functions/04-developing-in-Typescript.md b/site/book/05-developing-functions/04-developing-in-Typescript.md
index 1333ed77..dbca53e7 100644
--- a/site/book/05-developing-functions/04-developing-in-Typescript.md
+++ b/site/book/05-developing-functions/04-developing-in-Typescript.md
@@ -1,1 +1,241 @@
-TODO
+kpt project provides an opinionated Typescript SDK for implementing functions.
+It provides the following features:
+
+- **General-purpose language:** Similar to Go, Typescript is a general-purpose
+  programming language that provides:
+  - Proper abstractions and language features
+  - A extensive ecosystem of tooling (e.g. IDE support)
+  - A comprehensive catalog of well-supported libraries
+  - Robust community support and detailed documentation
+- **Idomatic:** The TS SDK provides a different level of abstraction compared to
+  the Go library we saw previously. Instead of exposing the low-level YAML AST,
+  resources are marshalled into native Typescript objects. As a result, you get
+  a more idiomatic and high-level abstraction. Ideally, you should work with
+  native data structure/object in each language and not think about YAML which
+  is just a file format. Even though resources in configuration files are
+  round-tripped to Typescript objects, the kpt orchestrator ensures that
+  YAML-specific constructs such as comments are preserved. The obvious
+  limitation of this high-level abstraction is that you cannot develop functions
+  for manipulating YAML-specific constructs like comments.
+- **Type-safety:** Kubernetes configuration is typed, and its schema is defined
+  using the OpenAPI spec. Typescript has a sophisticated type system that
+  accommodates the complexity of Kubernetes resources. The TS SDK enables
+  generating Typescript classes for core and CRD types.
+- **Batteries-included:** The TS SDK provides a simple, powerful API for
+  querying and manipulating resources inspired by [document-oriented databases].
+  It provides the scaffolding required to develop, build, test, and publish
+  functions, allowing you to focus on implementing your business-logic.
+
+## Quickstart
+
+This quickstart will get you started developing a kpt function with the TS SDK.
+
+### System Requirements
+
+Currently supported platforms: amd64 Linux/Mac
+
+- Install [kpt][download-kpt] and its dependencies
+- Install [node][download-node]
+  - The SDK requires `npm` version 6 or higher.
+  - If installing node from binaries (i.e. without a package manager), follow
+    these [installation instructions][install-node].
+
+### Explore the Demo Functions Package
+
+1. Get the `demo-functions` package:
+
+   ```shell
+   $ git clone --depth 1 https://github.com/GoogleContainerTools/kpt-functions-sdk.git
+   ```
+
+   All subsequent commands are run from the `demo-functions` directory:
+
+   ```shell
+   $ cd kpt-functions-sdk/ts/demo-functions
+   ```
+
+1. Install all dependencies:
+
+   ```shell
+   $ npm install
+   ```
+
+1. Run the following in a separate terminal to continuously build your function
+   as you make changes:
+
+   ```shell
+   $ npm run watch
+   ```
+
+1. Explore the [`label_namespace`][label-namespace] transformer function. This
+   function takes a given `label_name` and `label_value` to add the appropriate
+   label to `Namespace` objects using the SDK's `addLabel` function.
+
+   ```typescript
+   import { addLabel, Configs } from "kpt-functions";
+   import { isNamespace } from "./gen/io.k8s.api.core.v1";
+
+   export const LABEL_NAME = "label_name";
+   export const LABEL_VALUE = "label_value";
+
+   export async function labelNamespace(configs: Configs) {
+     const labelName = configs.getFunctionConfigValueOrThrow(LABEL_NAME);
+     const labelValue = configs.getFunctionConfigValueOrThrow(LABEL_VALUE);
+     configs
+       .get(isNamespace)
+       .forEach((n) => addLabel(n, labelName, labelValue));
+   }
+   ```
+
+1. Run the `label_namespace` function on the `example-configs` directory:
+
+   ```shell
+   $ export CONFIGS=../../example-configs
+
+   $ kpt fn source $CONFIGS \
+     | node dist/label_namespace_run.js -d label_name=color -d label_value=orange \
+     | kpt fn sink $CONFIGS
+   ```
+
+   As the name suggests, this function added the given label to all `Namespace`
+   objects in the `example-configs` directory:
+
+   ```shell
+   $ git diff $CONFIGS
+   ```
+
+1. Try modifying the function in `src/label_namespace.ts` to perform other
+   operations and rerun the function on `example-configs` to see the changes.
+
+1. Explore validation functions like [validate-rolebinding]. Instead of
+   transforming configuration, this function searches RoleBindings and returns a
+   results field containing details about invalid subject names.
+
+   ```typescript
+   import { Configs, kubernetesObjectResult } from "kpt-functions";
+   import { isRoleBinding } from "./gen/io.k8s.api.rbac.v1";
+
+   const SUBJECT_NAME = "subject_name";
+
+   export async function validateRolebinding(configs: Configs) {
+     // Get the subject name parameter.
+     const subjectName = configs.getFunctionConfigValueOrThrow(SUBJECT_NAME);
+
+     // Iterate over all RoleBinding objects in the input
+     // Filter those that have a subject we're looking for.
+     const results = configs
+       .get(isRoleBinding)
+       .filter(
+         (rb) =>
+           rb && rb.subjects && rb.subjects.find((s) => s.name === subjectName)
+       )
+       .map((rb) =>
+         kubernetesObjectResult(`Found banned subject '${subjectName}'`, rb)
+       );
+
+     configs.addResults(...results);
+   }
+   ```
+
+1. Run `validate-rolebinding` on `example-configs`.
+
+   ```shell
+   $ kpt fn source $CONFIGS \
+     | node dist/validate_rolebinding_run.js -d subject_name=alice@foo-corp.com \
+     | kpt fn sink $CONFIGS
+   $ git diff $CONFIGS
+   ```
+
+1. Explore generator functions like [expand-team-cr]. This function generates a
+   per-environment Namespace and RoleBinding object for each custom resource
+   (CR) of type Team.
+
+   ```typescript
+   import { Configs } from "kpt-functions";
+   import { isTeam, Team } from "./gen/dev.cft.anthos.v1alpha1";
+   import { Namespace } from "./gen/io.k8s.api.core.v1";
+   import { RoleBinding, Subject } from "./gen/io.k8s.api.rbac.v1";
+
+   const ENVIRONMENTS = ["dev", "prod"];
+
+   export async function expandTeamCr(configs: Configs) {
+     // For each 'Team' custom resource in the input:
+     // 1. Generate a per-enviroment Namespace.
+     // 2. Generate RoleBindings in each Namespace.
+     configs.get(isTeam).forEach((team) => {
+       const name = team.metadata.name;
+
+       ENVIRONMENTS.forEach((suffix) => {
+         const ns = `${name}-${suffix}`;
+         configs.insert(Namespace.named(ns));
+         configs.insert(...createRoleBindings(team, ns));
+       });
+     });
+   }
+
+   function createRoleBindings(team: Team, namespace: string): RoleBinding[] {
+     return (team.spec.roles || []).map((item) => {
+       return new RoleBinding({
+         metadata: {
+           name: item.role,
+           namespace,
+         },
+         subjects: roleSubjects(item),
+         roleRef: {
+           kind: "ClusterRole",
+           name: item.role,
+           apiGroup: "rbac.authorization.k8s.io",
+         },
+       });
+     });
+   }
+
+   function roleSubjects(item: Team.Spec.Item): Subject[] {
+     const userSubjects: Subject[] = (item.users || []).map(
+       (user) =>
+         new Subject({
+           kind: "User",
+           name: user,
+         })
+     );
+     const groupSubjects: Subject[] = (item.groups || []).map(
+       (group) =>
+         new Subject({
+           kind: "Group",
+           name: group,
+         })
+     );
+     return userSubjects.concat(groupSubjects);
+   }
+   ```
+
+1. Run `expand-team-cr` on `example-configs`.
+
+   ```shell
+   $ kpt fn source $CONFIGS \
+    | node dist/expand_team_cr_run.js \
+    | kpt fn sink $CONFIGS
+   $ git diff $CONFIGS
+   ```
+
+## Next Steps
+
+- Read the complete [Typescript SDK Developer Guide].
+- Take a look at these [example functions] to better understand how to use the
+  TS SDK.
+
+[download-kpt]: /book/01-getting-started/01-system-requirements
+[download-node]: https://nodejs.org/en/download/
+[install-node]: https://github.com/nodejs/help/wiki/Installation/
+[ts sdk api]: https://googlecontainertools.github.io/kpt-functions-sdk/api/
+[label-namespace]:
+  https://github.com/GoogleContainerTools/kpt-functions-sdk/blob/master/ts/demo-functions/src/label_namespace.ts
+[validate-rolebinding]:
+  https://github.com/GoogleContainerTools/kpt-functions-sdk/blob/master/ts/demo-functions/src/validate_rolebinding.ts
+[expand-team-cr]:
+  https://github.com/GoogleContainerTools/kpt-functions-sdk/blob/master/ts/demo-functions/src/expand_team_cr.ts
+[example functions]:
+  https://github.com/GoogleContainerTools/kpt-functions-sdk/tree/master/ts/demo-functions/src/
+[document-oriented databases]:
+  https://en.wikipedia.org/wiki/Document-oriented_database
+[typescript sdk developer guide]: /sdk/ts-guide
diff --git a/site/sdk/ts-guide.md b/site/sdk/ts-guide.md
new file mode 100644
index 00000000..a5850eb4
--- /dev/null
+++ b/site/sdk/ts-guide.md
@@ -0,0 +1,245 @@
+# Typescript SDK Developer Guide
+
+This guide will walk you through developing a kpt function using the Typescript SDK.
+
+## Setup
+
+### System Requirements
+
+Currently supported platforms: amd64 Linux/Mac
+
+### Setting Up Your Local Environment
+
+Currently supported platforms: amd64 Linux/Mac
+
+- Install [kpt][download-kpt] and its dependencies
+- Install [node][download-node]
+  - The SDK requires `npm` version 6 or higher.
+  - If installing node from binaries (i.e. without a package manager), follow
+    these [installation instructions][install-node].
+
+### Your Kubernetes Cluster
+
+For the type generation functionality to work, you need a Kubernetes cluster
+with the [CRD OpenAPI Publishing][crd-openapi] feature which is GA with
+Kubernetes 1.16.
+
+Alternatively, you can use an existing NPM package with pre-generated types
+such as the `hello-world` package discussed in the [Quickstart] and skip to
+[implementing the function](#implement-the-function).
+
+### Working with CRDs
+
+The SDK uses the k8s server to generate the Typescript classes. If your
+function uses a Custom Resource Definition, make sure you apply it to the
+cluster used for type generation:
+
+```sh
+$ kubectl apply -f /path/to/my/crd.yaml
+```
+
+## Create the NPM Package
+
+To initialize a new NPM package, first create a package directory:
+
+```sh
+$ mkdir my-package
+$ cd my-package
+```
+
+> **Note:** All subsequent commands are run from the `my-package/` directory.
+
+Run the interactive initializer:
+
+```sh
+$ npm init kpt-functions
+```
+
+Follow the instructions and respond to all prompts.
+
+This process will create the following:
+
+1. `package.json`: The `kpt-functions` framework library is the only item
+   declared in `dependencies`. Everything required to compile and test your
+   config function is declared as `devDependencies`, including the
+   `create-kpt-functions` CLI discussed later in the `README`.
+1. `src/`: Directory containing the source files for all your functions, e.g.:
+   - `my_func.ts`: Implement your function's interface here.
+   - `my_func_test.ts`: Unit tests for your function.
+   - `my_func_run.ts`: The entry point from which your function is run.
+1. `src/gen/`: Contains Kubernetes core and CRD types generated from the
+   OpenAPI spec published by the cluster you selected.
+1. `build/`: Contains Dockerfile for each function, e.g.:
+   - `my_func.Dockerfile`
+
+Next, install all package dependencies:
+
+```shell
+$ npm install
+```
+
+In addition to installation, `install` compiles your function into the `dist/`
+directory.
+
+You can run your function directly:
+
+```shell
+$ node dist/my_func_run.js --help
+```
+
+Currently, it simply passes through the input configuration data. Let's remedy
+this.
+
+## Implement the Function
+
+You can now start implementing the function using your favorite IDE, e.g.
+[VSCode]:
+
+```shell
+$ code .
+```
+
+In `src/my_func.ts`, implement the `KptFunc` interface from the [TS SDK API].
+
+Take a look at these [demo functions] to better understand how
+to use the typescript library.
+
+Once you've written some code, build the package with:
+
+```shell
+$ npm run build
+```
+
+Alternatively, run the following in a separate terminal. It will continuously
+build your function as you make changes:
+
+```shell
+$ npm run watch
+```
+
+To run the tests, use:
+
+```shell
+$ npm test
+```
+
+## Debug and Test the Function
+
+You may want to run a function developed with one of the config function SDKs
+using the exec runtime in order to avoid the overhead associated with running
+a container. To run your function in the exec runtime, you will first need to
+package your function as an executable.
+
+The below example shows how to run a typescript function using the kpt exec
+runtime.
+
+### Prerequisites
+
+- Install the pkg CLI.
+
+  ```shell
+  $ npm install -g pkg
+  ```
+
+- Install your kpt-functions package module to create your function's
+  distributable file.
+
+  ```shell
+  $ npm i
+  ```
+
+### Steps
+
+1. Use the pkg CLI to create an executable from your function's distributable
+   file. For a `my_fn` function built using the typescript SDK, this is
+   `dist/my_fn_run.js`.
+
+   ```shell
+   $ npx pkg dist/my_fn_run.js
+   ```
+
+1. Pass the path to the appropriate executable for your OS when running kpt
+   using the exec runtime.
+
+   ```shell
+   $ kpt fn run DIR/ --enable-exec --exec-path /path/to/my_fn_run-macos -- a=b
+   ```
+
+## Build and push container images
+
+With your working function in-hand, it's time to package your function into an
+executable container image.
+
+To build the docker image:
+
+```shell
+$ npm run kpt:docker-build
+```
+
+You can now run the function container, e.g.:
+
+```shell
+$ docker run gcr.io/kpt-functions-demo/my-func:dev --help
+```
+
+To push the image to your container registry of choice:
+
+```shell
+$ npm run kpt:docker-push
+```
+
+You'll need proper authentication/authorization to push to your registry.
+
+`kpt:docker-push` pushes to the registry specified in the
+`kpt.docker_repo_base` field in `package.json`. You can manually edit this
+field at any time.
+
+The default value for the container image tag is `dev`. This can be overridden
+using`--tag` flag:
+
+```shell
+$ npm run kpt:docker-build -- --tag=latest
+$ npm run kpt:docker-push -- --tag=latest
+```
+
+## Use the SDK CLI
+
+The `create-kpt-functions` package (installed as `devDependencies`), provides
+a CLI for managing the NPM package you created above. The CLI sub-commands can
+be invoked via `npm run`. For example, to add a new function to the package:
+
+```shell
+$ npm run kpt:function-create
+```
+
+These sub-commands are available:
+
+```
+kpt:docker-create       Generate Dockerfiles for all functions. Overwrite
+                        files if they exist.
+kpt:docker-build        Build container images for all functions.
+kpt:docker-push         Push container images to the registry for all
+                        functions.
+kpt:function-create     Generate stubs for a new function. Overwrites files
+                        if they exist.
+kpt:type-create         Generate classes for core and CRD types. Overwrite
+                        files if they exist.
+```
+
+Flags are passed to the CLI after the `--` separator. For example, to pass a
+tag when building a container image:
+
+```shell
+$ npm run kpt:docker-build -- --tag=latest
+```
+
+[download-kpt]: /book/01-getting-started/01-system-requirements
+[download-node]: https://nodejs.org/en/download/
+[install-node]: https://github.com/nodejs/help/wiki/Installation/
+[install-node]: https://github.com/nodejs/help/wiki/Installation/
+[install-docker]: https://docs.docker.com/engine/installation/
+[crd-openapi]: https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG/CHANGELOG-1.15.md#customresourcedefinition-openapi-publishing
+[quickstart]: ../quickstart/
+[vscode]: https://code.visualstudio.com/
+[ts sdk api]: https://googlecontainertools.github.io/kpt-functions-sdk/api/
+[demo functions]: https://github.com/GoogleContainerTools/kpt-functions-sdk/tree/master/ts/demo-functions/src/
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)
-}


Test patch:
diff --git a/e2e/testdata/fn-eval/consistent-formatting/.expected/diff.patch b/e2e/testdata/fn-eval/consistent-formatting/.expected/diff.patch
index a8d96bdb..5b210d26 100644
--- a/e2e/testdata/fn-eval/consistent-formatting/.expected/diff.patch
+++ b/e2e/testdata/fn-eval/consistent-formatting/.expected/diff.patch
@@ -1,75 +1,42 @@
 diff --git a/Kptfile b/Kptfile
-index 74670e7..9d4cd4a 100644
+index 74670e7..154c558 100644
 --- a/Kptfile
 +++ b/Kptfile
-@@ -2,30 +2,25 @@ apiVersion: kpt.dev/v1alpha2
+@@ -2,16 +2,7 @@ apiVersion: kpt.dev/v1alpha2
  kind: Kptfile
  metadata:
    name: nginx
-+  namespace: staging
- info:
-   description: describe this package
+-info:
+-  description: describe this package
 -  site: https://github.com/GoogleContainerTools/kpt
-   emails:
-     - foo@gmail.com
+-  emails:
+-    - foo@gmail.com
 -  license: license text
-   keywords:
-     - tag1
-     - tag2
-+  license: license text
-   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
+-  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
-+inventory:
-+  name: inventory-00933591
-+  namespace: some-space
-+  labels:
-+    foo: bar
-+  annotations:
-+    abc: def
-+  inventoryID: 92c234b7e9267815b0c3e17c9e4d7139a16c104f-1620493522822890000
++  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
-@@ -39,11 +34,17 @@ pipeline:
-       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
-+upstream:
-+  type: git
-+  updateStrategy: resource-merge
-+  git:
-+    directory: package-examples/nginx
-+    ref: v0.2
-+    repo: https://github.com/GoogleContainerTools/kpt
-+upstreamLock:
-+  type: git
-+  git:
-+    commit: 4d2aa98b45ddee4b5fa45fbca16f2ff887de9efb
-+    directory: package-examples/nginx
-+    ref: package-examples/nginx/v0.2
-+    repo: https://github.com/GoogleContainerTools/kpt
 diff --git a/resources.yaml b/resources.yaml
 index c0f48b0..9e31a48 100644
 --- a/resources.yaml
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-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 74%
rename from e2e/testdata/fn-render/path-index-current/db/Kptfile
rename to e2e/testdata/fn-render/generator-absolute-path/Kptfile
index 142ea1fd..30810450 100644
--- a/e2e/testdata/fn-render/path-index-current/db/Kptfile
+++ b/e2e/testdata/fn-render/generator-absolute-path/Kptfile
@@ -5,4 +5,4 @@ metadata:
 pipeline:
   mutators:
     - image: gcr.io/kpt-fn/starlark:unstable
-      configPath: starlark-httpbin-gen.yaml
+      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 100%
rename from e2e/testdata/fn-render/path-index-current/db/resources.yaml
rename to e2e/testdata/fn-render/generator-absolute-path/resources.yaml
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/inline-fnconfig/Kptfile b/e2e/testdata/fn-render/inline-fnconfig/Kptfile
index 5a2a6ef8..0f9559d2 100644
--- a/e2e/testdata/fn-render/inline-fnconfig/Kptfile
+++ b/e2e/testdata/fn-render/inline-fnconfig/Kptfile
@@ -8,7 +8,7 @@ pipeline:
       configMap:
         namespace: staging
     - image: gcr.io/kpt-fn/set-label:unstable
-      config: # inline config
+      config:
         apiVersion: v1
         kind: ConfigMap
         data:
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-current/.expected/diff.patch b/e2e/testdata/fn-render/path-index-current/.expected/diff.patch
index 6212e438..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
diff --git a/e2e/testdata/fn-render/path-index-current/Kptfile b/e2e/testdata/fn-render/path-index-current/Kptfile
index 7b3bc56d..375117ba 100644
--- a/e2e/testdata/fn-render/path-index-current/Kptfile
+++ b/e2e/testdata/fn-render/path-index-current/Kptfile
@@ -2,3 +2,7 @@ apiVersion: kpt.dev/v1alpha2
 kind: Kptfile
 metadata:
   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 100%
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
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-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/validate-generated-resource/Kptfile b/e2e/testdata/fn-render/validate-generated-resource/Kptfile
index 15001326..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
+    - image: gcr.io/kpt-fn/starlark:unstable
       configPath: starlark-httpbin-val.yaml
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/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/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
 }