Repository: GoogleContainerTools/kpt
Pull request #2589 (2021-11-16 01:08:29)

Docs: OCI Support design docs
Adding PR with design doc, as companion to oci-support branch.




Patch:
diff --git a/Makefile b/Makefile
index bb94d116..b9f4d028 100644
--- a/Makefile
+++ b/Makefile
@@ -87,7 +87,7 @@ test-fn-eval: build
 
 # target to run e2e tests for "kpt fn eval" command
 test-live-apply: build
-	PATH=$(GOBIN):$(PATH) go test -v -timeout=20m --tags=kind --run=TestLiveApply/testdata/live-apply/$(T)  ./e2e/
+	PATH=$(GOBIN):$(PATH) go test -v -timeout=20m --tags=kind -p 2 --run=TestLiveApply/testdata/live-apply/$(T)  ./e2e/
 
 vet:
 	go vet ./...
diff --git a/docs/design-docs/00-template.md b/docs/design-docs/00-template.md
index 38d2b548..e6ce78f3 100644
--- a/docs/design-docs/00-template.md
+++ b/docs/design-docs/00-template.md
@@ -70,4 +70,17 @@ here as well as citing *why* you decided not to pursue those paths.
 ### \<Approach\>
 
 Links and description of the approach, the pros and cons identified during the 
-design. 
\ No newline at end of file
+design.
+
+# Review Process
+
+Design doc creators should raise the pull request similar to [this](https://github.com/GoogleContainerTools/kpt/pull/2576).
+Reviewers are auto-assigned to review the PR. Optionally, doc owner can mention any
+specific reviewers on the PR description. The turn around time for each review cycle
+on the PR from kpt maintainers is 1-2 days. After maintainers add comments to the PR,
+doc owner should respond to each of those comments and hit `Resolve conversation` button on the 
+comment thread. Once all the comments are resolved, design doc creator should hit `Re-request review`
+icon next to each reviewers' avatar in reviewers section. Reviewers will then be notified and, they will go through the
+resolved comments and might reopen the comment threads, and the cycle continues.
+Once all the reviewers approve the PR, it will be merged by the maintainers.
+Any design doc PRs which are not attended by the doc creators for more than 2 weeks will be closed.
diff --git a/docs/design-docs/01-live_Inventory-to-ResourceGroup.md b/docs/design-docs/01-live_Inventory-to-ResourceGroup.md
new file mode 100644
index 00000000..ce2e50f8
--- /dev/null
+++ b/docs/design-docs/01-live_Inventory-to-ResourceGroup.md
@@ -0,0 +1,297 @@
+# Title
+
+* Author(s): Yuwen Ma <yuwenma@google.com>
+* Contributor(s):
+    - Sean Sullivan <seans@google.com>
+    - Sunil Arora <sunilarora@google.com>
+    - Mortent Torkildsen <mortent@google.com>
+    - Mengqi Yu <mengqiy@google.com>
+    - Natasha Sarkar <natashasarkar@google.com>
+
+* Approver: Sunil Arora <sunilarora@google.com>
+
+## Why
+
+This design is aimed at making `kpt live` be hydration-tool-neutral so that `kpt live`
+applicable use cases can be expanded to `kustomize build`, `helm templates` and raw
+kubernetes manifests.
+
+### Non-goal
+- This doc is not aimed at designing a unified hydration between kpt and kustomize
+- This doc is not aimed at changing the kpt in-place hydration models.
+
+## Design
+
+We propose to make `kpt live` support reading configurations from standard input,
+detach the Inventory from Kptfile and simplify the Inventory-ResourceGroup mechanism
+to use  ResourceGroup only.
+
+### Read standard input resources
+
+We will change the existing `kpt live apply` from STDIN to not require the existence of Kptfile (
+to be more specific, the `inventory` file from Kptfile). As long as the STDIN contains
+one and only one valid ResourceGroup, `kpt live apply` should be able to create/match the
+ResourceGroup in cluster.
+
+### Initialize a `ResourceGroup` object
+
+`kpt live init` will create a ResourceGroup CR in resourcegroup.yaml. 
+
+By default, the ResoureGroup will have name and namespace assigned as below. And users can
+override via existing flags "--name" and "--namespace".
+
+- metadata.name: A client-provided valid RFC 1123 DNS subdomain name. Default value has prefix “inventory-” with
+  8 digit numbers. E.g. "inventory-02014045"
+- metadata.namespace: a valid namespace. default to value "default"
+
+If users want to reuse an existing inventory (from Kptfile) or ResourceGroup (which has been deployed to the cluster),
+they shall provide the value of the inventory's inventory-id or the ResourceGroup's "metadata.labels[0].cli-utils.sigs.k8s.io/inventory-id"
+via "--inventory-id" flag.
+
+### Convert Inventory to ResourceGroup
+
+The client-side Inventory is considered as the identifier of the cluster-side
+ResourceGroup CR. Right now, the Inventory is stored in the Kptfile to guarantee
+the ResourceGroup is unique. This requires Kptfile to exist to use `kpt live apply`.
+
+To split the Inventory from Kptfile to resourcegroup.yaml and convert the Inventory to
+ResourceGroup, `kpt live migrate` should be extended to map the inventory.name,
+inventory.namespace, inventory.inventoryID to ResourceGroup CR metadata.name,
+metadata.namespace, metadata.labels.cli-utils.sigs.k8s.io/inventory-id correspondingly.
+
+Inventory in Kptfile
+```yaml
+apiVersion: kpt.dev/v1
+kind: Kptfile
+inventory:
+  name: <INVENTORY_NAME>
+  namespace: <INVENTORY_NAMESPACE>
+  inventoryID: <INVENTORY_ID>
+```
+resoucegroup.yaml
+```yaml
+apiVersion: kpt.dev/v1alpha1
+kind: ResourceGroup
+metadata  
+  name: <INVENTORY_NAME>
+  namespace: <INVENTORY_NAMESPACE>
+  labels:
+    cli-utils.sigs.k8s.io/inventory-id: <INVENTORY_ID>
+```
+
+### Simplify the Inventory
+
+Current inventory contains inventory-id which is required to match the label `cli-utils.sigs.k8s.io/inventory-id`.
+
+For new users, they should no longer need to be exposed to the inventory-id, but kpt will
+build one composed by `name-namespace` on the fly. 
+
+For existing users, the inventory-id is still
+required in the standalone ResourceGroup file to guarantee the adoption matches, unless they use "--inventory-policy=adopt"
+to override the label. This flag is only required as a one-off via `kpt live apply -` to override the label to `name-namespace`.
+
+### ResourceGroup as a singleton object
+
+`kpt live apply [--rg-file] -` from STDIN accepts and only accepts a single
+ResourceGroup, including the ResourceGroup provided by the flag. It detects
+1. If more than one ResourceGroup is found, raise errors and display all the ResourceGroup objects.
+2. If no ResourceGroup is found in STDIN and and Kptfile inventory does not exists, raise errors and suggest users to
+   run kpt live init
+3. If no ResourceGroup is found in STDIN and Kptfile inventory exists, raise errors and
+   suggest users to run kpt live migrate
+
+### New flags
+
+#### `--rg-file` 
+
+- description: The file path to the ResourceGroup CR, default to `resourcegroup.yaml`
+- short form `--rg`
+- This flag will be added to `kpt live init`, `kpt live migrate` and `kpt live apply`
+
+#### `--name` for inventory
+
+- description: The name for the ResourceGroup
+- This flag will continue to be used by `kpt live init`. Rather than overriding the
+  inventory.name in Kptfile, it will override the default metadata.name in the standalone ResourceGroup file.
+
+#### `--namespace` for inventory
+
+- description: The namespace for the ResourceGroup
+- This flag will continue to be used by `kpt live init`, Rather than overriding the
+  inventory.namespace in Kptfile, it will override the default metadata.namespace in the standalone ResourceGroup file.
+
+#### `--inventory-id` for inventory
+
+- description: Inventory identifier. This is used to detect overlap between
+  two sets of ResourceGroup managed resources that might use the same name and namespace.
+- This flag will continue to be accepted by `kpt live init` for backward compatibility reasons. 
+  If given, ResourceGroup will store the inventory-id value in "metadata.labels[0].cli-utils.sigs.k8s.io/inventory-id"
+  of the ResourceGroup. 
+  If not given, the ResourceGroup labels will be empty and the value of "<name>-<namespace>" will be
+  used as the "cli-utils.sigs.k8s.io/inventory-id" label in `kpt live apply` from STD.
+
+### `resoucegroup.yaml` interaction with `kpt fn` commands
+
+All `kpt fn` commands should treat `resoucegroup.yaml` as a special resource 
+and not modify it in any scenario. It should not be considered as a meta resource and 
+`include-meta-resources` flag should not include the `ResourceGroup` resource in 
+the `ResourceList`. In the future, we can add a new flag `--include-rg` if there are
+valid use-cases to modify or include `ResourceGroup` resource in `ResourceList`.
+
+## User Guide
+
+### To hydrate via kustomize and deploy via kpt
+
+#### Day 1
+
+##### For new users to start from scratch (no Kptfile)
+User can run `kpt live init [--rg-file=resourcegroup.yaml]` to create a
+ResourceGroup object and store it in a resourcegroup.yaml file.
+Users can customize the file path with the flag “--rg-file”.
+
+##### For existing kpt users to migrate from Kptfile
+Users run `kpt live migrate [--rg-file=resourcegroup.yaml]` to convert the
+Inventory object from Kptfile to a standalone resourcegroup.yaml file.
+Users can customize the file path with the flag “--rg-file”.
+
+##### [optional]: Add shareable ResourceGroup to kustomize resources
+If the ResourceGroup is expected to be shared in the Gitops workflow, users can add
+the resourcegroup.yaml  file path to the .resources field in kustomization.yaml.
+This simplifies the Day N deployment by omitting the “–rg-file“ flag.
+
+#### Day N
+
+- Users can configure the hydration rules in kustomization.yaml
+- Users can run `kustomize build <DIR> | kpt live apply -` to hydrate and deploy
+  the kustomize-managed configurations. If resourcegroup.yaml is not added to
+  kustomize .resources field, users should provide the “–rg-file“ flag.  
+  `kustomize build <DIR> | kpt live apply –rg-file <resourcegroup.yaml> -`
+
+### To hydrate via helm and deploy via kpt
+
+#### Day 1
+
+##### For new users to start from scratch (no Kptfile)
+User can run `kpt live init --rg-file=<DIR>/resourcegroup.yaml` to create
+a ResourceGroup object and store it in the helm template <DIR>.
+
+##### For existing kpt users to migrate from Kptfile
+Users run `kpt live migrate --rg-file=<DIR>/resourcegroup.yaml` to convert
+the Inventory object from Kptfile to  a standalone resourcegroup.yaml file.
+
+#### Day N
+
+Users can run `helm templates <DIR> | kpt live apply -` to hydrate and deploy the
+helm-managed resources.
+
+See kpt issue #2399 for expected Inventory usage in package publisher/consumer.
+
+## FAQ
+
+### Will Inventory be deprecated from Kptfile?
+
+Kpt still supports inventory in Kptfile and it is not required to migrate to the
+standalone rg-path. In fact, users who do not use STDIN in `kpt live apply`
+will still have inventory read from Kptfile by default unless the –inventory-path flag
+is given.
+
+### Why not kpt live apply -k?
+
+We originally propose to use a single command kpt live apply -k (similar idea as
+kubectl apply -k ) to kustomize-hydrate and kpt-deploy the resources, storing the
+inventory in the annotation fields of the kustomization.yaml (See below).
+The inventory can be auto added to kustomization.yaml by kpt live init
+```yaml
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+metadata:
+  annotations:
+     kpt-inventory-id:
+     kpt-inventory-name:
+     kpt-inventory-namespace: 
+```
+#### Pros
+
+- Extreme simple user steps. Kustomize users only need to install kpt and run kpt live
+  init. Then they can start using kpt live apply to deploy their kustomize resources.
+  No manual changes to any configurations.
+- Easy to understand. The command is similar to  kubectl apply -k which makes it easier
+  to be accepted and remembered.
+
+There are two main reasons to abandon this approach:
+- Making Kpt a hydration-neutral deployment tool helps it better hook up with different
+  hydration tools.
+- kpt is not planning to treat kustomize differently and thus it does not make sense to
+  provide kustomize a shortcut.
+
+## Alternatives Considered
+
+### Use Inventory metadata to store ResourceGroup info
+
+Store the Kptfile inventory to a standalone Inventory object. Since the inventory is
+the metadata of the cluster side ResourceGroup,  hiding the ResourceGroup behind
+inventory provides users simpler syntax and mitigates the probability of overriding
+the ResourceGroup unintentionally (e.g. via kubectl apply).
+
+inventory.yaml
+```yaml
+apiVersion: kpt.dev/v1
+kind: Inventory
+metadata:
+  name: <INVENTORY_NAME>
+  namespace: <INVENTORY_NAMESPACE>
+inventoryID: <INVENTORY_ID>
+```
+
+#### Pros
+
+- Simpler resource syntax
+- Mitigate unintentional resourceGroup override.
+
+#### Cons
+- The client-side inventory requires users to manage.
+  This new resource increases the overall complexity of the kpt actuation config.
+  We frequently receive user confusions about the difference between ResourceGroup
+  and Inventory and why they cannot configure inventory like other kubernetes resource
+  (adoption mismatch errors).
+
+### Use Inventory spec to store ResourceGroup info
+
+
+inventory.yaml
+```yaml
+apiVersion: kpt.dev/v1
+kind: Inventory
+spec:
+  resourceGroup:
+    name: <INVENTORY_NAME>
+    Namespace: <INVENTORY_NAMESPACE>
+    labels:
+      cli-utils.sigs.k8s.io/inventory-id: <INVENTORY_ID>
+```
+#### Pros
+
+- Obey the kubernetes convention and make it clearer that users are expected to provide the resourceGroup kind with name, namespace and labels.
+- Mitigate unintentional resourceGroup override.
+
+#### Cons
+More complex syntax than ResourceGroup or Inventory metadata solution
+
+### Add a controller for ResourceGroup pruning; No client-side Inventory
+
+Inventory is a [cli-utils object](https://github.com/kubernetes-sigs/cli-utils/blob/52b000849deb2b233f0a58e9be16ca2725a4c9cf/pkg/inventory/inventory.go#L30) used to store the [ResourceGroup metadata](https://docs.google.com/document/d/1x_02xGKZH6YGuc3K-KNeY4KmADPAwzrlQKkvTNXYekc/edit#heading=h.265kfx5ku27).
+It is stored in Kptfile .Inventory and required by `kpt live apply`. In lieu of a user configuration, it is more of a data for kpt to identify the corresponding ResourceGroup for pruning. This adds non-trivial overheads for users to understand, maintain and retrieve the inventory.
+
+The alternative proposal is to remove the inventory but use a new controller to prune the resource.
+The controller will be installed together with ResourceGroup CRD (in each `kpt live apply`).
+It will find the previous ResourceGroup CR from current resources, three-way merging and deploying
+the new resources and update the ResourceGroup.
+
+#### Pros
+- Improve the user experience on using kpt live and minimize the confusions around inventory and ResourceGroup
+- Eliminate the pain points on retrieving the missing Inventory (list all ResourceGroup, check the .spec.resources and try one by one until not encounter the “missing adoption” error).
+
+#### Cons
+The controller needs multiple requests to find out the previous ResourceGroup CR. This causes heavy traffic in a production cluster.  
+The controller cannot 100% guarantee the resourceGroup reverse lookup are accurate.
+Since the pruning happens on the cluster side, kpt can no longer provide real-time status updates.
diff --git a/docs/design-docs/01-oci-support.md b/docs/design-docs/01-oci-support.md
new file mode 100644
index 00000000..11d925d0
--- /dev/null
+++ b/docs/design-docs/01-oci-support.md
@@ -0,0 +1,474 @@
+# OCI Support
+
+* Author(s): Louis Dejardin, @loudej
+* Approver: \<kpt-maintainer\>
+
+>    Every feature will need design sign off an PR approval from a core
+>    maintainer.  If you have not got in touch with anyone yet, you can leave
+>    this blank and we will try to line someone up for you.
+
+## Why
+
+Systems that deal with packaging software or bundling configuration often have
+an atomic, versionable artifact. This artifact can exist as a source of
+truth independant from the source controlled content from which it was built.
+
+It is also very common for those packages to have an associated feed or repository
+which can receive those packages as they are published, and make them available for 
+download as needed. In many companies, using `git` as the as the repository and source
+of truth for production configuration comes with challenges.
+
+This design document proposes to add `OCI` as an alternative to `git` for publishing and
+distributing Kpt config packages. As a packaging format, it is well understood and documented.
+As a repository format, it leverages existing container registries for pushing and pulling
+config as image content. For security and production configuration management, if a 
+company has practices for managing Docker container images in private registries, then
+the same practices and security model can be applied to config package images in private
+registries.
+
+https://github.com/GoogleContainerTools/kpt/issues/2300
+
+## Design
+
+### Design Assumptions
+
+The first stage of OCI support comes from adding support for `oci` in the places
+where `git` appears today.
+
+An image tag is used in the same way a git branch or tag would be used.
+
+An image digest is used in the same way a git commit would be used.
+
+The scope of a single image is one root package, with any number of optional sub-packages.
+
+The structure of the image is a single tar layer. The root `Kptfile` is in the base directory from the tar layer's point of view. It contains only the Kpt package files, no entrypoint or executables.
+
+A package image should not be confused with a container image. Container images are executable by software, like `Docker`, and package images are purely configuration data.
+
+### Config chages
+
+The `Kptfile` structures for `upstream` and `upstreamLock` have `oci` in addition to `git` properties. The `type` property also has the string `oci` added as an accepted value.
+
+```yaml
+upstream:
+  type: oci
+  oci:
+    image: 'IMAGE:TAG'
+upstreamLock:
+  type: oci
+  oci:
+    image: 'IMAGE:DIGEST'
+```
+
+New verions of `kpt` will support existing `Kptfile`. The file structures and `git` functionality is unchanged.
+
+Existing versions of `kpt` will support `Kptfile` with `upstream` based on `git` for the same reason. The structure and meaning of existing fields is not changed.
+
+Existing versions of `kpt` will not support `Kptfile` with `upstream` based on `oci`. The `type` value, and missing `git` information will fail validation. The `kpt` binary used will need to be upgraded.
+
+### Command changes
+
+### `kpt pkg get`
+
+The argument that determines upstream today is parsed into `repo`, `ref`, and `path`, and is implicitly a `git` location.
+
+To support `oci`, it will be necessary to extract different values in a way that's unambiguous. Unfortunately, OCI image names have no Uri prefix, and are indistinguishable from a valid path or file name.
+
+To solve this, using [Helm](https://helm.sh/docs/topics/registries/#other-subcommands) as an example, the prefix `oci://` can be used. This ensures that selecting `oci` protocol isn't accidental, and it won't collide with other location formats that may be added.
+
+```shell
+# clone package as new folder
+kpt pkg get oci://us-docker.pkg.dev/the-project-id/the-repo-name/the-package:v3 my-package
+```
+
+Because OCI image reference already has a convention for `image:tag` references, using `:v3` should be used instead of `@v3` for version. It will be more intuitive how it relates to the registry, and easier to cut and paste values.
+
+### `kpt pkg get` sub-packages
+
+It is possible to use `kpt pkg get` to add sub-packages to a target location.
+
+Syntax for a sub-package target location is unchanged, it's a normal filesystem path.
+
+Syntax for an OCI sub-package source location requires the ability to tell when an image name ends and a sub-package path inside that image begins. In `git` this requires an explicit `.git` extension at the transition, and in `.oci` this requires double slash.
+
+```shell
+# clone sub-package as new sub-folder
+kpt pkg get oci://us-docker.pkg.dev/the-project-id/the-repo-name/the-package//simple/example:v3 my-package/simple/my-example
+```
+
+### `kpt pkg update`
+
+The command for update is not changed, but when the `upstream` is `oci` then the `@VERSION` is used to change the `upstream` image's `tag` or `digest` value.
+
+To update to an image tag, `kpt pkg update @v14` and `kpt pkg update DIR@v14` will assign the `:v14` tag onto the upstream image.
+
+```yaml
+upstream:
+  type: oci
+  oci:
+    image: us-docker.pkg.dev/the-project-id/the-repo-name/the-package:v14
+```
+
+To update to an upstream digest, `kpt pkg update @sha256:{SHA256_HEX}` and `kpt pkg update DIR@sha256:{SHA256_HEX}` will assign `@sha256:{SHA256_HEX}` as the new upstream image digest.
+
+```yaml
+upstream:
+  type: oci
+  oci:
+    image: us-docker.pkg.dev/the-project-id/the-repo-name/the-package@sha256:8815143a333cb9d2cb341f10b984b22f3b8a99fe
+```
+
+Calling `kpt pkg update` and `kpt pkg update DIR` will perform an update without changing the upstream image name.
+
+At that point, if the `upstream` is an `image:tag` that is to discover the current `image:digest` for tag, otherwise the `upstream` value for `image:digest` is used. In either case, the `upstreamLock` is changed to point at that new `image:digest`. 
+
+The package contents of the old and new `upstreamLock` image digest are fetched to temp folders, and are the basis of the 3-way merge to update the target package.
+
+### `kpt pkg diff`
+
+The `kpt pkg diff` command is identical to `kpt pkg update` in the way that `[PKG_PATH@VERSION]` argument is mapped to OCI concepts.
+
+### Command additions
+
+Although it is possible to create and push an OCI image using a combination of commands like `tar` and `gcrane`, that
+doesn't provide a very complete end to end experience. Because kpt would already be built with the same OCI go module used 
+by `gcrane`, it is not difficult to support additional commands to move pull and push package contents from local folders
+to remote images and back.
+
+### ` kpt pkg pull`
+
+```
+Usage: kpt pkg pull oci://{IMAGE[:TAG|@sha256:DIGEST]} [DIR] 
+  DIR                         Destination folder for image contents. Default folder name is the last part of the IMAGE path.
+  IMAGE[:TAG|@sha256:DIGEST]  Name of image to pull contents from, with optional TAG or DIGEST. Default TAG is `Latest`
+```
+
+This command is the reverse of push. An image can be pulled from a repository to a local folder, modified, and pushed
+back to the same location, same location with different TAG, or entirely different location.
+
+The target DIR is optional, following the conventions of `kpg pkg get`, and will default to the final image/path segment.
+
+`kpt pkg pull` works on git uri as well. This may be used, for example, to mirror a set of known blueprints into a private 
+oci registry.
+
+### ` kpt pkg push`
+
+```
+Usage: kpt pkg push [DIR@VERSION] [--origin oci://{IMAGE[:TAG]}] [--increment]
+  DIR@VERSION     Folder containing package root Kptfile. Default is current directory.
+                  Optional @VERSION changes tag or branch to push onto. Default is most recently pulled or pushed tag.
+  --origin        Name of image to push contents onto, with optional TAG to assign to resulting commit.
+                  Default is to use most recently pulled/pushed image. Required if Kptfile does not have an origin.
+  --increment     Increase the version by 1 while pushing. Default is to leave the origin's TAG or DIR@VERSION unchanged.
+                  The Kptfile's image TAG is also updated to the new value. 
+```
+
+This command will `tar` the contents of the package into a single image layer, and push it into the OCI repository. For
+Google Artifact Registry and Google Container Registry, the current `gcloud auth` SSO credentials are used.
+
+The simplest form of the command is `kpt pkg push` or `kpt pkg push DIR` which will push the current contents back to
+the IMAGE:TAG location that was saved when `kpt pkg pull` was run.
+
+The synxax `kpt pkg push @VERSION` or `kpt pkg push DIR@VERSION` will push back to the image location it came from, but with a new TAG name or version. Examples are `kpt pkt push @draft` or `kpt pkg push @v4`
+
+If the Kptfile was not obtained by `kpt pkg pull` - for example it's a new package from `kpt pkg init` or `kpt pkg get` - then
+the first `kpt pkg push` will require an `--origin IMAGE:TAG` option to provide the target location. It is only necessary on the first
+call.
+
+Finally, if the IMAGE's TAG value is a valid version number, the `--increment` switch can be used to add 1 to the current value before pushing.
+
+In the simplest case a `v1` is changed to `v2`, and `1` is changed to `2`, but any TAG that is a valid semver (with optional leading 'v') will have the smallest part of the number incremented. So `v1.0` becomes `v1.1`, `v1.0.0` becomes `v1.0.1`, and `v4.1.9-alpha` becomes `v4.1.10-alpha`
+
+#### Comparison of `pkg get` and `pkg pull`
+
+Starting with a simple root package, and an orange variant with root as the upstream:
+
+```
+-- root
+  \-- orange {upstream: root}
+```
+
+The purpose of `kpt get` is to create a new leaf node. This is done by creating the initial copy of the new leaf package in a
+local folder. This has the side-effects of altering the kptfile name, the upstream values to point at the source, and makes appropriate 
+changes to sub-package metadata.
+
+As an example, after running `kpt pkg get scheme://repo/root green` and `kpt pkg get scheme://repo/orange blue` the `green` and 
+`blue` local folder packages are appended to the inheritance tree like this:
+
+```
+-- root
+  \-- orange {upstream: root}
+  | `-- blue {upstream: orange}  ** working copy in ./blue **
+  \-- green {upstream: root}     ** working copy in ./green **
+```
+
+By comparison, `kpt pkg pull` does not create a new package node or identity - it only extracts a copy of existing package
+contents to a working directory. In this example, if the user additionally ran `kpt pkg pull scheme://repo/root root` and 
+`kpt pkg pull scheme://repo/orange orange` the overall state would be this:
+
+```
+-- root                          ** working copy in ./root **
+  \-- orange {upstream: root}    ** working copy in ./orange **
+  | `-- blue {upstream: orange}  ** working copy in ./blue **
+  \-- green {upstream: root}     ** working copy in ./green **
+```
+
+### Alternatives to push/pull
+
+There are several ways that pull and push could appear as commands. Those two names are very conventional, but 
+alternatives to consider could be:
+
+### `kpt pkg copy`
+
+```
+Usage: kpt pkg copy {SOURCE} {DEST}
+  SOURCE  Package source location: a local DIR, or `oci://` image, or git repo and path
+  DEST    Package destination: a local DIR, or `oci://` image.
+```
+
+Puts a copy of the SOURCE package at the DEST location. The package contents would be entirely unchanged by this operation (unlike `kpt pkg get`).
+
+To pull from remote image to local folder:
+
+```
+kpt pkg copy \
+  oci://us-docker.pkg.dev/the-project-id/the-repo-name/the-package:v14 \
+  the-package
+```
+
+To push from local folder to new remote image tag:
+
+```
+kpt pkg copy \
+  the-package \
+  oci://us-docker.pkg.dev/the-project-id/the-repo-name/the-package:v15
+```
+ 
+To copy a package image from one OCI repo to another:
+
+```
+kpt pkg copy \
+  oci://us-docker.pkg.dev/the-project-id/dev-blueprints/the-package:v25 \
+  oci://us-docker.pkg.dev/the-project-id/prod-blueprints/the-package:v25
+```
+
+To copy a package from a git location to an OCI repo:
+
+```
+kpt pkg copy \
+  https://github.com/GoogleCloudPlatform/blueprints.git/catalog/gke@main \
+  oci://us-docker.pkg.dev/the-project-id/gcp-catalog/gke:latest
+```
+
+## User Guide
+
+### Creating a package respository
+
+Before kpt packages can be pushed and pulled as OCI images, a suitable repository 
+must be created. Google Artifact Registry and Google Container Registry are both
+excellent choices.
+
+```shell
+# Choose names and locations
+LOCATION="us"
+PROJECT_ID="kpt-demo-73823"
+REPOSITORY_NAME="blueprints"
+
+# Base name for any images in this repository
+REPOSITORY="${LOCATION}-docker.pkg.dev/${PROJECT_ID}/${REPOSITORY_NAME}"
+
+# Create the repository
+gcloud artifacts repositories create --location="${LOCATION}" --repository-format=docker --project="${PROJECT_ID}" "${REPOSITORY_NAME}"
+```
+
+### Creating and pushing a new package
+
+Creating a new package is no different. But when ready to publish, the `kpt pkg push` command 
+is used instead of source control operations.
+
+```shell
+# A package in a new directory
+mkdir hello-world
+kpt pkg init hello-world --description="A simple blueprint"
+
+# Store the contents in the repository, tagged as v1
+kpt pkg push hello-world --image=${REPOSITORY}/hello-world:v1
+
+# The local files are not needed any more, pushing has stored them all
+rm -r hello-world
+```
+
+### Pulling and updating a package
+
+Because the package folder was discarded earlier, the `kpt pkg pull` command 
+is used to place the contents of a particular version at a location. These set of
+commands may be run in `Cloud Build` steps as well, if you are automating the
+publication of packages as part of a CI/CD process.
+
+```shell
+# Recreate the folder and extract the pulled image
+kpt pkg pull hello-world --image=${REPOSITORY}/hello-world:v1
+
+# Add a sub-package from a git repo
+kpt pkg get https://github.com/GoogleCloudPlatform/blueprints.git/catalog/bucket hello-world/my-bucket
+
+# Render to be sure contents are hydrated, and push to a new version tag
+kpt pkg render hello-world
+kpt pkg push hello-world --image=${REPOSITORY}/hello-world:v2
+```
+
+Similar to container image tags, the package image tags like `:v1` and `:v2` above may be used any
+number of ways based on your preferred workflows. The tag `:latest` is used by default if all
+pulls and pushes should read from and overwrite the same location. Semantic tags like `:draft` and
+environmental tags like `:dev`, `:qa`, and `:prod` may be also used.
+
+No matter what tags are used, the image repository and `kpt` cli will treat them as an alphanumeric 
+label.
+
+### Using OCI repository as an upstream
+
+In addition to providing storage for packages, an OCI registry may also 
+be used as a source of upstream images to clone. The `oci://` prefix on this
+command is required to ensure 
+
+```shell
+# Clone the hello-world v1 blueprint into a new folder
+kpt pkg get oci://${REPOSITORY}/hello-world:v1 greetings-planet
+
+# Push the results to the repository, using default `latest` tag in this example
+kpt pkg push greetings-planet --image=${REPOSITORY}/greetings-planet
+```
+
+Looking in the `greetings-planet/Kptfile` at this point will show that
+the `hello-world:v1` image is the `upstream`, and the `upstreamLock` will show
+exactly the digest that this clone is up-to-date with. 
+
+```yaml
+apiVersion: kpt.dev/v1
+kind: Kptfile
+metadata:
+  name: greetings-planet
+upstream:
+  type: oci
+  oci:
+    image: us-docker.pkg.dev/kpt-demo-73823/blueprints/hello-world:v1
+  updateStrategy: resource-merge
+upstreamLock:
+  type: oci
+  oci:
+    image: us-docker.pkg.dev/kpt-demo-73823/blueprints/hello-world@sha256:1632e00af3fe858c5e3b3f9e75c16e6327449155
+```
+
+### Adding a subpackage from an OCI upstream subfolder
+
+Often a folder inside a package is meant to be used as a way to create "more of the same".
+
+To use an OCI image subfolder as the source of a subpackage, the path is added in a 
+way that's distinct from the image itself.
+
+```shell
+# Clone the hello-world v1 blueprint into a new folder
+kpt pkg get oci://${REPOSITORY}/hello-world//my-bucket:v1 greetings-planet/another-bucket
+```
+
+The `greetings-planet` package will now contain both a `greetings-planet/my-bucket` as well as a
+`greetings-planet/another-bucket` folder. The contents in locations will now both receive changes
+when the upstream `hello-world/my-bucket` is updated.
+
+### Updating package with upstream changes
+
+The value of the upstream image tag is used to `kpt pkg update` to a specific version.
+This works no matter if the tag appears to look like a version number or not.
+
+```shell
+# Update the greetings-planet by applying any differences between the upstreamLock digest and the `v2` tag
+kpt pkg update greetings-planet@v2
+
+# Overwrite the `greetings-planet:latest` image with the folder contents
+kpt pkg push greetings-planet --image=${REPOSITORY}/greetings-planet
+```
+
+The Kptfile will now show that the `upstream` and `upstreamLock` have both been changed.
+
+```yaml
+apiVersion: kpt.dev/v1
+kind: Kptfile
+metadata:
+  name: greetings-planet
+upstream:
+  type: oci
+  oci:
+    image: us-docker.pkg.dev/kpt-demo-73823/blueprints/hello-world:v2
+  updateStrategy: resource-merge
+upstreamLock:
+  type: oci
+  oci:
+    image: us-docker.pkg.dev/kpt-demo-73823/blueprints/hello-world@sha256:a6f1ed69c6ab51e2a148f6d4926bccb24c843887
+```
+
+Just like with a git upstream, it is also possible to `kpt pkg update` without providing a different
+tag or version value. This is similar to pulling from a remote branch where the name of the branch does 
+change but the latest commit on that branch is a different hash.
+
+In that case the Kptfile `upstream` tag will not change, but if that tag has been overwritten with
+new contents then the differences between `upstreamLock` and current contents will be applied to the local copy,
+and the `upstreamLock` will be changed tot he current digest.
+
+### Updating package to a specific upstream push
+
+Much like you can `kpt pkg update` to a specific commit hash in git, you can update to an
+exact image digest with OCI. This is an even more precise reference than by a version number because,
+like git commits, the image digest is based on the package file contents and cannot be forged or altered.
+
+```shell
+# Update instread to an exact image digest of the upstream location
+kpt pkg update greetings-planet@sha256:3b42daa41102fa83bce07bd82a72edcd691868d6
+```
+
+The resulting Kptfile in the local folder will look like this.
+
+```yaml
+apiVersion: kpt.dev/v1
+kind: Kptfile
+metadata:
+  name: greetings-planet
+upstream:
+  type: oci
+  oci:
+    image: us-docker.pkg.dev/kpt-demo-73823/blueprints/hello-world@sha256:3b42daa41102fa83bce07bd82a72edcd691868d6
+  updateStrategy: resource-merge
+upstreamLock:
+  type: oci
+  oci:
+    image: us-docker.pkg.dev/kpt-demo-73823/blueprints/hello-world@sha256:3b42daa41102fa83bce07bd82a72edcd691868d6
+```
+
+## Open Issues/Questions
+
+> Please list any open questions here in the following format:
+> 
+> ### \<Question\>
+> 
+> Resolution: Please list the resolution if resolved during the design process or
+> specify __Not Yet Resolved__
+
+### What additional container registries should be supported?
+
+The protocol and information is the same. It would mainly be a question
+of how the credentials for the call are provided.
+
+### What commands on exising kpt binary will work on Kptfile with `oci`
+
+It may be possible `kpt` commands that to not process `upstream` structures
+may not require update to work correctly. `kpt fn` and `kpt live` commands
+should be tested to see how they behave.
+
+## Alternatives Considered
+
+If there is an industry precedent or alternative approaches please list them 
+here as well as citing *why* you decided not to pursue those paths.
+
+### \<Approach\>
+
+Links and description of the approach, the pros and cons identified during the 
+design. 
diff --git a/go.mod b/go.mod
index 6a48efa8..3e387586 100644
--- a/go.mod
+++ b/go.mod
@@ -15,15 +15,16 @@ require (
 	github.com/stretchr/testify v1.7.0
 	github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca
 	gotest.tools v2.2.0+incompatible
-	k8s.io/api v0.22.2
+	k8s.io/api v0.22.3
 	k8s.io/apiextensions-apiserver v0.22.2
-	k8s.io/apimachinery v0.22.2
+	k8s.io/apimachinery v0.22.3
 	k8s.io/cli-runtime v0.22.2
 	k8s.io/client-go v0.22.2
 	k8s.io/klog/v2 v2.10.0
+	k8s.io/kube-openapi v0.0.0-20211109043139-026bd182f079 // indirect
 	k8s.io/kubectl v0.22.2
 	k8s.io/utils v0.0.0-20210820185131-d34e5cb4466e
 	sigs.k8s.io/cli-utils v0.26.1-0.20211020064957-d62b5c62002d
 	sigs.k8s.io/kustomize/api v0.8.11
-	sigs.k8s.io/kustomize/kyaml v0.12.1-0.20211012224254-55ac9ca88db9
+	sigs.k8s.io/kustomize/kyaml v0.13.1-0.20211203194734-cd2c6a1ad117
 )
diff --git a/go.sum b/go.sum
index 8634f219..90451494 100644
--- a/go.sum
+++ b/go.sum
@@ -988,12 +988,14 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
 honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
-k8s.io/api v0.22.2 h1:M8ZzAD0V6725Fjg53fKeTJxGsJvRbk4TEm/fexHMtfw=
 k8s.io/api v0.22.2/go.mod h1:y3ydYpLJAaDI+BbSe2xmGcqxiWHmWjkEeIbiwHvnPR8=
+k8s.io/api v0.22.3 h1:wOoES2GoSkUsdped2RB4zYypPqWtvprGoKCENTOOjP4=
+k8s.io/api v0.22.3/go.mod h1:azgiXFiXqiWyLCfI62/eYBOu19rj2LKmIhFPP4+33fs=
 k8s.io/apiextensions-apiserver v0.22.2 h1:zK7qI8Ery7j2CaN23UCFaC1hj7dMiI87n01+nKuewd4=
 k8s.io/apiextensions-apiserver v0.22.2/go.mod h1:2E0Ve/isxNl7tWLSUDgi6+cmwHi5fQRdwGVCxbC+KFA=
-k8s.io/apimachinery v0.22.2 h1:ejz6y/zNma8clPVfNDLnPbleBo6MpoFy/HBiBqCouVk=
 k8s.io/apimachinery v0.22.2/go.mod h1:O3oNtNadZdeOMxHFVxOreoznohCpy0z6mocxbZr7oJ0=
+k8s.io/apimachinery v0.22.3 h1:mrvBG5CZnEfwgpVqWcrRKvdsYECTrhAR6cApAgdsflk=
+k8s.io/apimachinery v0.22.3/go.mod h1:O3oNtNadZdeOMxHFVxOreoznohCpy0z6mocxbZr7oJ0=
 k8s.io/apiserver v0.22.2/go.mod h1:vrpMmbyjWrgdyOvZTSpsusQq5iigKNWv9o9KlDAbBHI=
 k8s.io/cli-runtime v0.22.2 h1:fsd9rFk9FSaVq4SUq1fM27c8CFGsYZUJ/3BkgmjYWuY=
 k8s.io/cli-runtime v0.22.2/go.mod h1:tkm2YeORFpbgQHEK/igqttvPTRIHFRz5kATlw53zlMI=
@@ -1005,16 +1007,19 @@ k8s.io/component-base v0.22.2/go.mod h1:5Br2QhI9OTe79p+TzPe9JKNQYvEKbq9rTJDWllun
 k8s.io/component-helpers v0.22.2/go.mod h1:+N61JAR9aKYSWbnLA88YcFr9K/6ISYvRNybX7QW7Rs8=
 k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
 k8s.io/gengo v0.0.0-20201214224949-b6c5ce23f027/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
+k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
 k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
 k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
 k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec=
 k8s.io/klog/v2 v2.10.0 h1:R2HDMDJsHVTHA2n4RjwbeYXdOcBymXdX/JRb1v0VGhE=
 k8s.io/klog/v2 v2.10.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec=
-k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e h1:KLHHjkdQFomZy8+06csTWZ0m1343QqxZhR2LJ1OxCYM=
 k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw=
+k8s.io/kube-openapi v0.0.0-20211109043139-026bd182f079 h1:Qvr4c6wr4iBZbZQY/IwkcJH04hpdzUt2zdJw4D7P/Ng=
+k8s.io/kube-openapi v0.0.0-20211109043139-026bd182f079/go.mod h1:X90lRFlqk35/w9FG4WIvZqMPfG3WrZGzdlSaL6uh7rc=
 k8s.io/kubectl v0.22.2 h1:KMyYNZoBshaL3XKx04X07DtpoD4vMrdkfiN/G2Qx/PU=
 k8s.io/kubectl v0.22.2/go.mod h1:BApg2j0edxLArCOfO0ievI27EeTQqBDMNU9VQH734iQ=
 k8s.io/metrics v0.22.2/go.mod h1:GUcsBtpsqQD1tKFS/2wCKu4ZBowwRncLOJH1rgWs3uw=
+k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
 k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
 k8s.io/utils v0.0.0-20210820185131-d34e5cb4466e h1:ldQh+neBabomh7+89dTpiFAB8tGdfVmuIzAHbvtl+9I=
 k8s.io/utils v0.0.0-20210820185131-d34e5cb4466e/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
@@ -1032,8 +1037,8 @@ sigs.k8s.io/kustomize/cmd/config v0.9.13/go.mod h1:7547FLF8W/lTaDf0BDqFTbZxM9zqw
 sigs.k8s.io/kustomize/kustomize/v4 v4.2.0/go.mod h1:MOkR6fmhwG7hEDRXBYELTi5GSFcLwfqwzTRHW3kv5go=
 sigs.k8s.io/kustomize/kyaml v0.11.0/go.mod h1:GNMwjim4Ypgp/MueD3zXHLRJEjz7RvtPae0AwlvEMFM=
 sigs.k8s.io/kustomize/kyaml v0.12.0/go.mod h1:FTJxEZ86ScK184NpGSAQcfEqee0nul8oLCK30D47m4E=
-sigs.k8s.io/kustomize/kyaml v0.12.1-0.20211012224254-55ac9ca88db9 h1:YsD7ModmQptMeuSZW/ZOeV2/LLQaOYNUA3sx3yZQTa0=
-sigs.k8s.io/kustomize/kyaml v0.12.1-0.20211012224254-55ac9ca88db9/go.mod h1:FTJxEZ86ScK184NpGSAQcfEqee0nul8oLCK30D47m4E=
+sigs.k8s.io/kustomize/kyaml v0.13.1-0.20211203194734-cd2c6a1ad117 h1:4ePwpEnROoPqjaY6WC98EiRpYrmJjDHV6KA7PGDmpdQ=
+sigs.k8s.io/kustomize/kyaml v0.13.1-0.20211203194734-cd2c6a1ad117/go.mod h1:FTJxEZ86ScK184NpGSAQcfEqee0nul8oLCK30D47m4E=
 sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
 sigs.k8s.io/structured-merge-diff/v4 v4.1.2 h1:Hr/htKFmJEbtMgS/UD0N+gtgctAqz81t3nu+sPzynno=
 sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4=
diff --git a/internal/cmdliveinit/cmdliveinit.go b/internal/cmdliveinit/cmdliveinit.go
index 5335dc61..c16792c4 100644
--- a/internal/cmdliveinit/cmdliveinit.go
+++ b/internal/cmdliveinit/cmdliveinit.go
@@ -16,6 +16,7 @@ import (
 	"github.com/GoogleContainerTools/kpt/internal/errors"
 	"github.com/GoogleContainerTools/kpt/internal/pkg"
 	"github.com/GoogleContainerTools/kpt/internal/printer"
+	"github.com/GoogleContainerTools/kpt/internal/util/attribution"
 	kptfilev1 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1"
 	"github.com/GoogleContainerTools/kpt/pkg/kptfile/kptfileutil"
 	"github.com/spf13/cobra"
@@ -174,6 +175,10 @@ func (c *ConfigureInventoryInfo) Run(ctx context.Context) error {
 	if err != nil {
 		return errors.E(op, c.Pkg.UniquePath, err)
 	}
+	// add metrics annotation to package resources to track the usage as the resources
+	// will be applied using kpt live group
+	at := attribution.Attributor{PackagePaths: []string{c.Pkg.UniquePath.String()}, CmdGroup: "live"}
+	at.Process()
 	return nil
 }
 
diff --git a/internal/cmdrender/executor.go b/internal/cmdrender/executor.go
index b8663f25..b321a90e 100644
--- a/internal/cmdrender/executor.go
+++ b/internal/cmdrender/executor.go
@@ -27,6 +27,7 @@ import (
 	"github.com/GoogleContainerTools/kpt/internal/pkg"
 	"github.com/GoogleContainerTools/kpt/internal/printer"
 	"github.com/GoogleContainerTools/kpt/internal/types"
+	"github.com/GoogleContainerTools/kpt/internal/util/attribution"
 	"github.com/GoogleContainerTools/kpt/internal/util/printerutil"
 	fnresult "github.com/GoogleContainerTools/kpt/pkg/api/fnresult/v1"
 	kptfilev1 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1"
@@ -81,6 +82,11 @@ func (e *Executor) Execute(ctx context.Context) error {
 		return err
 	}
 
+	// add metrics annotation to output resources to track the usage as the resources
+	// are rendered by kpt fn group
+	at := attribution.Attributor{Resources: hctx.root.resources, CmdGroup: "fn"}
+	at.Process()
+
 	if e.Output == nil {
 		// the intent of the user is to modify resources in-place
 		pkgWriter := &kio.LocalPackageReadWriter{
@@ -473,7 +479,7 @@ func adjustRelPath(hctx *hydrationContext) error {
 		if err = r.PipeE(yaml.SetAnnotation(kioutil.PathAnnotation, newPath)); err != nil {
 			return err
 		}
-		if err = r.PipeE(yaml.SetAnnotation(kioutil.LegacyPathAnnotation, newPath)); err != nil {
+		if err = r.PipeE(yaml.SetAnnotation(kioutil.LegacyPathAnnotation, newPath)); err != nil { // nolint:staticcheck
 			return err
 		}
 		if err = pkg.RemovePkgPathAnnotation(r); err != nil {
diff --git a/internal/docs/generated/fndocs/docs.go b/internal/docs/generated/fndocs/docs.go
index 210f7413..f37a533c 100644
--- a/internal/docs/generated/fndocs/docs.go
+++ b/internal/docs/generated/fndocs/docs.go
@@ -90,7 +90,7 @@ Flags:
     Container image of the function to execute e.g. ` + "`" + `gcr.io/kpt-fn/set-namespace:v0.1` + "`" + `.
     For convenience, if full image path is not specified, ` + "`" + `gcr.io/kpt-fn/` + "`" + ` is added as default prefix.
     e.g. instead of passing ` + "`" + `gcr.io/kpt-fn/set-namespace:v0.1` + "`" + ` you can pass ` + "`" + `set-namespace:v0.1` + "`" + `.
-    ` + "`" + `eval` + "`" + ` executes only one function, so do not use ` + "`" + `--exec-path` + "`" + ` flag with this flag.
+    ` + "`" + `eval` + "`" + ` executes only one function, so do not use ` + "`" + `--exec` + "`" + ` flag with this flag.
   
   --image-pull-policy:
     If the image should be pulled before rendering the package(s). It can be set
diff --git a/internal/fnruntime/runner.go b/internal/fnruntime/runner.go
index 532d679c..bdbaa1a3 100644
--- a/internal/fnruntime/runner.go
+++ b/internal/fnruntime/runner.go
@@ -224,21 +224,24 @@ func parseStructuredResult(yml *yaml.RNode, fnResult *fnresult.Result) error {
 		return err
 	}
 
-	return parseNameAndNamespace(yml, fnResult)
+	return migrateLegacyResult(yml, fnResult)
 }
 
-// parseNameAndNamespace populates name and namespace in fnResult.Result if a
+// migrateLegacyResult populates name and namespace in fnResult.Result if a
 // function (e.g. using kyaml Go SDKs) gives results in a schema
 // that puts a resourceRef's name and namespace under a metadata field
 // TODO: fix upstream (https://github.com/GoogleContainerTools/kpt/issues/2091)
-func parseNameAndNamespace(yml *yaml.RNode, fnResult *fnresult.Result) error {
+func migrateLegacyResult(yml *yaml.RNode, fnResult *fnresult.Result) error {
 	items, err := yml.Elements()
 	if err != nil {
 		return err
 	}
 
 	for i := range items {
-		if err := populateResourceRef(items[i], &fnResult.Results[i]); err != nil {
+		if err = populateResourceRef(items[i], fnResult.Results[i]); err != nil {
+			return err
+		}
+		if err = populateProposedValue(items[i], fnResult.Results[i]); err != nil {
 			return err
 		}
 	}
@@ -246,7 +249,22 @@ func parseNameAndNamespace(yml *yaml.RNode, fnResult *fnresult.Result) error {
 	return nil
 }
 
-func populateResourceRef(item *yaml.RNode, resultItem *framework.ResultItem) error {
+func populateProposedValue(item *yaml.RNode, resultItem *framework.Result) error {
+	sv, err := item.Pipe(yaml.Lookup("field", "suggestedValue"))
+	if err != nil {
+		return err
+	}
+	if sv == nil {
+		return nil
+	}
+	if resultItem.Field == nil {
+		resultItem.Field = &framework.Field{}
+	}
+	resultItem.Field.ProposedValue = sv
+	return nil
+}
+
+func populateResourceRef(item *yaml.RNode, resultItem *framework.Result) error {
 	r, err := item.Pipe(yaml.Lookup("resourceRef", "metadata"))
 	if err != nil {
 		return err
@@ -282,7 +300,7 @@ func printFnResult(ctx context.Context, fnResult *fnresult.Result, opt *printer.
 		// function returned structured results
 		var lines []string
 		for _, item := range fnResult.Results {
-			lines = append(lines, resultToString(item))
+			lines = append(lines, item.String())
 		}
 		ri := &multiLineFormatter{
 			Title:          "Results",
@@ -402,57 +420,6 @@ func (ri *multiLineFormatter) String() string {
 	return b.String()
 }
 
-// resultToString converts given structured result item to string format.
-func resultToString(result framework.ResultItem) string {
-	// TODO: Go SDK should implement Stringer method
-	// for framework.ResultItem. This is a temporary
-	// wrapper that will eventually be moved to Go SDK.
-
-	defaultSeverity := "info"
-
-	s := strings.Builder{}
-
-	severity := defaultSeverity
-
-	if string(result.Severity) != "" {
-		severity = string(result.Severity)
-	}
-	s.WriteString(fmt.Sprintf("[%s] %s", strings.ToUpper(severity), result.Message))
-
-	resourceID := resourceRefToString(result.ResourceRef)
-	if resourceID != "" {
-		// if an object is involved
-		s.WriteString(fmt.Sprintf(" in object %q", resourceID))
-	}
-
-	if result.File.Path != "" {
-		s.WriteString(fmt.Sprintf(" in file %q", result.File.Path))
-	}
-
-	if result.Field.Path != "" {
-		s.WriteString(fmt.Sprintf(" in field %q", result.Field.Path))
-	}
-
-	return s.String()
-}
-
-func resourceRefToString(ref yaml.ResourceIdentifier) string {
-	s := strings.Builder{}
-	if ref.APIVersion != "" {
-		s.WriteString(fmt.Sprintf("%s/", ref.APIVersion))
-	}
-	if ref.Kind != "" {
-		s.WriteString(fmt.Sprintf("%s/", ref.Kind))
-	}
-	if ref.Namespace != "" {
-		s.WriteString(fmt.Sprintf("%s/", ref.Namespace))
-	}
-	if ref.Name != "" {
-		s.WriteString(ref.Name)
-	}
-	return s.String()
-}
-
 func newFnConfig(f *kptfilev1.Function, pkgPath types.UniquePath) (*yaml.RNode, error) {
 	const op errors.Op = "fn.readConfig"
 	var fn errors.Fn = errors.Fn(f.Image)
diff --git a/internal/util/attribution/attribution.go b/internal/util/attribution/attribution.go
new file mode 100644
index 00000000..4543ec2c
--- /dev/null
+++ b/internal/util/attribution/attribution.go
@@ -0,0 +1,134 @@
+// 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 attribution
+
+import (
+	"fmt"
+	"os"
+	"strings"
+
+	"sigs.k8s.io/kustomize/kyaml/kio"
+	kyaml "sigs.k8s.io/kustomize/kyaml/yaml"
+)
+
+const (
+	CNRMMetricsAnnotation            = "cnrm.cloud.google.com/blueprint"
+	DisableKptAttributionEnvVariable = "KPT_DISABLE_ATTRIBUTION"
+)
+
+// Attributor is used to attribute the kpt action on resources
+type Attributor struct {
+	// PackagePaths is the package paths to add the attribution annotation
+	PackagePaths []string
+
+	// Resources to add the attribution annotation
+	Resources []*kyaml.RNode
+
+	// CmdGroup is the command groups in kpt, e.g., pkg, fn, live
+	CmdGroup string
+}
+
+// Process invokes Attribution kyaml filter on the resources in input packages paths
+func (a *Attributor) Process() {
+	// users can opt-out by setting the "KPT_DISABLE_ATTRIBUTION" environment variable
+	if os.Getenv(DisableKptAttributionEnvVariable) != "" {
+		return
+	}
+
+	if a.CmdGroup == "" {
+		return
+	}
+
+	for _, path := range a.PackagePaths {
+		inout := &kio.LocalPackageReadWriter{PackagePath: path, PreserveSeqIndent: true, WrapBareSeqNode: true}
+		err := kio.Pipeline{
+			Inputs:  []kio.Reader{inout},
+			Filters: []kio.Filter{kio.FilterAll(a)},
+			Outputs: []kio.Writer{inout},
+		}.Execute()
+		if err != nil {
+			// this should be a best effort, do not error if this step fails
+			// https://github.com/GoogleContainerTools/kpt/issues/2559
+			return
+		}
+	}
+
+	for _, resource := range a.Resources {
+		_, _ = a.Filter(resource)
+	}
+}
+
+// Filter implements kyaml.Filter
+// this filter adds "cnrm.cloud.google.com/blueprint" annotation to the resource
+// if the annotation is already present, it appends kpt-<cmdGroup> suffix
+// it uses "default" namespace
+func (a *Attributor) Filter(object *kyaml.RNode) (*kyaml.RNode, error) {
+	// users can opt-out by setting the "KPT_DISABLE_ATTRIBUTION" environment variable
+	if os.Getenv(DisableKptAttributionEnvVariable) != "" {
+		return object, nil
+	}
+
+	// add this annotation to only KCC resource types
+	if !strings.Contains(object.GetApiVersion(), ".cnrm.") {
+		return object, nil
+	}
+
+	curAnnoVal := object.GetAnnotations()[CNRMMetricsAnnotation]
+	mf := object.Field(kyaml.MetadataField)
+	if mf.IsNilOrEmpty() {
+		// skip adding merge comment if empty metadata
+		return object, nil
+	}
+	if _, err := object.Pipe(kyaml.SetAnnotation(CNRMMetricsAnnotation, recordAction(curAnnoVal, a.CmdGroup))); err != nil {
+		return object, nil
+	}
+	return object, nil
+}
+
+// recordAction appends the input cmdGroup to the annotation to attribute the usage
+// if the cmdGroup is already present, then it is no-op
+func recordAction(curAnnoVal, cmdGroup string) string {
+	if curAnnoVal == "" {
+		return fmt.Sprintf("kpt-%s", cmdGroup)
+	}
+	if !strings.Contains(curAnnoVal, "kpt-") {
+		// just append the value
+		return fmt.Sprintf("%s,kpt-%s", curAnnoVal, cmdGroup)
+	}
+	// we want to extract the current kpt part from the annotation
+	// value and make sure that the input cmdGroup is added
+	// e.g. curAnnoVal: cnrm/landing-zone:networking/v0.4.0,kpt-pkg,blueprints_controller
+	curAnnoParts := strings.Split(curAnnoVal, ",")
+
+	// form the new kpt part value
+	newKptPart := []string{"kpt"}
+
+	for i, curAnnoPart := range curAnnoParts {
+		if strings.Contains(curAnnoPart, "kpt") {
+			if strings.Contains(curAnnoPart, "pkg") || cmdGroup == "pkg" {
+				newKptPart = append(newKptPart, "pkg")
+			}
+			if strings.Contains(curAnnoPart, "fn") || cmdGroup == "fn" {
+				newKptPart = append(newKptPart, "fn")
+			}
+			if strings.Contains(curAnnoPart, "live") || cmdGroup == "live" {
+				newKptPart = append(newKptPart, "live")
+			}
+			// replace the kpt part with the newly formed part
+			curAnnoParts[i] = strings.Join(newKptPart, "-")
+		}
+	}
+	return strings.Join(curAnnoParts, ",")
+}
diff --git a/internal/util/cmdutil/cmdutil.go b/internal/util/cmdutil/cmdutil.go
index 5a365c30..8fda513e 100644
--- a/internal/util/cmdutil/cmdutil.go
+++ b/internal/util/cmdutil/cmdutil.go
@@ -165,7 +165,7 @@ func WriteToOutput(r io.Reader, w io.Writer, outDir string) error {
 		outputs = []kio.Writer{&kio.ByteWriter{
 			Writer: w,
 			ClearAnnotations: []string{kioutil.IndexAnnotation, kioutil.PathAnnotation,
-				kioutil.LegacyIndexAnnotation, kioutil.LegacyPathAnnotation}},
+				kioutil.LegacyIndexAnnotation, kioutil.LegacyPathAnnotation}}, // nolint:staticcheck
 		}
 	}
 
diff --git a/internal/util/diff/pkgdiff.go b/internal/util/diff/pkgdiff.go
index c4d3550e..7e1384b3 100644
--- a/internal/util/diff/pkgdiff.go
+++ b/internal/util/diff/pkgdiff.go
@@ -21,8 +21,10 @@ import (
 	"path/filepath"
 
 	"github.com/GoogleContainerTools/kpt/internal/pkg"
+	"github.com/GoogleContainerTools/kpt/internal/util/attribution"
 	"github.com/GoogleContainerTools/kpt/internal/util/pkgutil"
 	kptfilev1 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1"
+	"sigs.k8s.io/kustomize/kyaml/kio"
 	"sigs.k8s.io/kustomize/kyaml/sets"
 	"sigs.k8s.io/kustomize/kyaml/yaml"
 )
@@ -68,7 +70,7 @@ func PkgDiff(pkg1, pkg2 string) (sets.String, error) {
 			if err != nil {
 				return diff, err
 			}
-			if !bytes.Equal(b1, b2) {
+			if !nonKptfileEquals(string(b1), string(b2)) {
 				diff.Insert(f)
 			}
 		}
@@ -120,3 +122,29 @@ func pkgSet(pkgPath string) (sets.String, error) {
 	}
 	return pkgFiles, nil
 }
+
+// nonKptfileEquals returns true if contents of two non-Kptfiles are equal
+// since the changes to addmetricsannotation.CNRMMetricsAnnotation is made
+// by kpt, we should not treat it as changes made by user, so delete the annotation
+// before comparing
+func nonKptfileEquals(s1, s2 string) bool {
+	out1 := &bytes.Buffer{}
+	out2 := &bytes.Buffer{}
+	err := kio.Pipeline{
+		Inputs:  []kio.Reader{&kio.ByteReader{Reader: bytes.NewBufferString(s1)}},
+		Filters: []kio.Filter{kio.FilterAll(yaml.AnnotationClearer{Key: attribution.CNRMMetricsAnnotation})},
+		Outputs: []kio.Writer{kio.ByteWriter{Writer: out1}},
+	}.Execute()
+	if err != nil {
+		return bytes.Equal([]byte(s1), []byte(s2))
+	}
+	err = kio.Pipeline{
+		Inputs:  []kio.Reader{&kio.ByteReader{Reader: bytes.NewBufferString(s2)}},
+		Filters: []kio.Filter{kio.FilterAll(yaml.AnnotationClearer{Key: attribution.CNRMMetricsAnnotation})},
+		Outputs: []kio.Writer{kio.ByteWriter{Writer: out2}},
+	}.Execute()
+	if err != nil {
+		return bytes.Equal([]byte(s1), []byte(s2))
+	}
+	return out1.String() == out2.String()
+}
diff --git a/internal/util/get/get.go b/internal/util/get/get.go
index 47cfabe8..70b1d658 100644
--- a/internal/util/get/get.go
+++ b/internal/util/get/get.go
@@ -29,10 +29,12 @@ import (
 	"github.com/GoogleContainerTools/kpt/internal/printer"
 	"github.com/GoogleContainerTools/kpt/internal/types"
 	"github.com/GoogleContainerTools/kpt/internal/util/addmergecomment"
+	"github.com/GoogleContainerTools/kpt/internal/util/attribution"
 	"github.com/GoogleContainerTools/kpt/internal/util/fetch"
 	"github.com/GoogleContainerTools/kpt/internal/util/stack"
 	kptfilev1 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1"
 	"github.com/GoogleContainerTools/kpt/pkg/kptfile/kptfileutil"
+	"sigs.k8s.io/kustomize/kyaml/kio"
 )
 
 // Command fetches a package from a git repository, copies it to a local
@@ -98,9 +100,16 @@ func (c Command) Run(ctx context.Context) error {
 		return cleanUpDirAndError(c.Destination, err)
 	}
 
-	if err := addmergecomment.Process(c.Destination); err != nil {
-		return cleanUpDirAndError(c.Destination, err)
-	}
+	inout := &kio.LocalPackageReadWriter{PackagePath: c.Destination, PreserveSeqIndent: true, WrapBareSeqNode: true}
+	amc := &addmergecomment.AddMergeComment{}
+	at := &attribution.Attributor{PackagePaths: []string{c.Destination}, CmdGroup: "pkg"}
+	// do not error out as this is best effort
+	_ = kio.Pipeline{
+		Inputs:  []kio.Reader{inout},
+		Filters: []kio.Filter{kio.FilterAll(amc), kio.FilterAll(at)},
+		Outputs: []kio.Writer{inout},
+	}.Execute()
+
 	return nil
 }
 
diff --git a/internal/util/merge/merge3.go b/internal/util/merge/merge3.go
index 4ca77ab9..effdf27b 100644
--- a/internal/util/merge/merge3.go
+++ b/internal/util/merge/merge3.go
@@ -18,6 +18,7 @@ import (
 	"path/filepath"
 	"strings"
 
+	"github.com/GoogleContainerTools/kpt/internal/util/attribution"
 	kptfilev1 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1"
 	"sigs.k8s.io/kustomize/kyaml/kio"
 	"sigs.k8s.io/kustomize/kyaml/kio/filters"
@@ -357,7 +358,8 @@ func (*resourceHandler) equals(r1, r2 *yaml.RNode) (bool, error) {
 
 func stripKyamlAnnos(n *yaml.RNode) error {
 	for _, a := range []string{mergeSourceAnnotation, kioutil.PathAnnotation, kioutil.IndexAnnotation,
-		kioutil.LegacyPathAnnotation, kioutil.LegacyIndexAnnotation} {
+		kioutil.LegacyPathAnnotation, kioutil.LegacyIndexAnnotation, // nolint:staticcheck
+		kioutil.InternalAnnotationsMigrationResourceIDAnnotation, attribution.CNRMMetricsAnnotation} {
 		err := n.PipeE(yaml.ClearAnnotation(a))
 		if err != nil {
 			return err
diff --git a/internal/util/update/resource-merge.go b/internal/util/update/resource-merge.go
index 64681bbe..da20a990 100644
--- a/internal/util/update/resource-merge.go
+++ b/internal/util/update/resource-merge.go
@@ -25,6 +25,7 @@ import (
 	"github.com/GoogleContainerTools/kpt/internal/errors"
 	"github.com/GoogleContainerTools/kpt/internal/pkg"
 	"github.com/GoogleContainerTools/kpt/internal/types"
+	pkgdiff "github.com/GoogleContainerTools/kpt/internal/util/diff"
 	"github.com/GoogleContainerTools/kpt/internal/util/merge"
 	"github.com/GoogleContainerTools/kpt/internal/util/pkgutil"
 	kptfilev1 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1"
@@ -125,7 +126,7 @@ func (u ResourceMergeUpdater) updatePackage(subPkgPath, localPath, updatedPath,
 	// Package deleted from upstream
 	case originalExists && localExists && !updatedExists:
 		// Check the diff. If there are local changes, we keep the subpackage.
-		diff, err := copyutil.Diff(originalPath, localPath)
+		diff, err := pkgdiff.PkgDiff(originalPath, localPath)
 		if err != nil {
 			return errors.E(op, types.UniquePath(localPath), err)
 		}
diff --git a/pkg/api/fnresult/v1/types.go b/pkg/api/fnresult/v1/types.go
index fb125c52..e6dc7848 100644
--- a/pkg/api/fnresult/v1/types.go
+++ b/pkg/api/fnresult/v1/types.go
@@ -38,7 +38,7 @@ type Result struct {
 	// ExitCode is the exit code from running the function
 	ExitCode int `yaml:"exitCode"`
 	// Results is the list of results for the function
-	Results []framework.ResultItem `yaml:"results,omitempty"`
+	Results framework.Results `yaml:"results,omitempty"`
 }
 
 const (
diff --git a/pkg/api/kptfile/v1/types.go b/pkg/api/kptfile/v1/types.go
index 03203830..a79e33f2 100644
--- a/pkg/api/kptfile/v1/types.go
+++ b/pkg/api/kptfile/v1/types.go
@@ -42,7 +42,7 @@ var TypeMeta = yaml.ResourceMeta{
 // KptFile contains information about a package managed with kpt.
 // swagger:model kptfile
 type KptFile struct {
-	yaml.ResourceMeta `yaml:",inline"`
+	yaml.ResourceMeta `yaml:",inline" json:",inline"`
 
 	Upstream *Upstream `yaml:"upstream,omitempty" json:"upstream,omitempty"`
 
diff --git a/pkg/live/rgpath.go b/pkg/live/rgpath.go
index 1abd36c8..99d32a3b 100644
--- a/pkg/live/rgpath.go
+++ b/pkg/live/rgpath.go
@@ -57,7 +57,7 @@ func (r *ResourceGroupPathManifestReader) Read() ([]*unstructured.Unstructured,
 			continue
 		}
 
-		if err := removeAnnotations(n, kioutil.IndexAnnotation, kioutil.LegacyIndexAnnotation); err != nil {
+		if err := removeAnnotations(n, kioutil.IndexAnnotation, kioutil.LegacyIndexAnnotation); err != nil { // nolint:staticcheck
 			return objs, err
 		}
 		u, err := kyamlNodeToUnstructured(n)
diff --git a/pkg/live/rgstream.go b/pkg/live/rgstream.go
index 6e83c825..2ea25966 100644
--- a/pkg/live/rgstream.go
+++ b/pkg/live/rgstream.go
@@ -55,7 +55,7 @@ func (p *ResourceGroupStreamManifestReader) Read() ([]*unstructured.Unstructured
 			continue
 		}
 
-		err = removeAnnotations(n, kioutil.IndexAnnotation, kioutil.LegacyIndexAnnotation)
+		err = removeAnnotations(n, kioutil.IndexAnnotation, kioutil.LegacyIndexAnnotation) // nolint:staticcheck
 		if err != nil {
 			return objs, err
 		}
diff --git a/site/book/03-packages/08-publishing-a-package.md b/site/book/03-packages/08-publishing-a-package.md
index 9de94703..1cd2ddbf 100644
--- a/site/book/03-packages/08-publishing-a-package.md
+++ b/site/book/03-packages/08-publishing-a-package.md
@@ -20,7 +20,7 @@ $ git tag v0.1
 Push the commit which requires you to have access to the repo:
 
 ```shell
-$ git push v0.1
+$ git push origin v0.1
 ```
 
 You can then fetch the published package:
@@ -46,7 +46,7 @@ $ git tag packages/wordpress/v0.1
 Push the commit:
 
 ```shell
-$ git push packages/wordpress/v0.1
+$ git push origin packages/wordpress/v0.1
 ```
 
 You can then fetch the published package:
diff --git a/site/installation/README.md b/site/installation/README.md
index bbe17c2e..4a783d26 100644
--- a/site/installation/README.md
+++ b/site/installation/README.md
@@ -70,7 +70,7 @@ Use one of the kpt docker images.
 ### `kpt`
 
 ```shell
-$ docker run gcr.io/kpt-dev/kpt:v1.0.0-beta.7 version
+$ docker run gcr.io/kpt-dev/kpt:v1.0.0-beta.9 version
 ```
 
 ### `kpt-gcloud`
@@ -78,7 +78,7 @@ $ docker run gcr.io/kpt-dev/kpt:v1.0.0-beta.7 version
 An image which includes kpt based upon the Google [cloud-sdk] alpine image.
 
 ```shell
-$ docker run gcr.io/kpt-dev/kpt-gcloud:v1.0.0-beta.7 version
+$ docker run gcr.io/kpt-dev/kpt-gcloud:v1.0.0-beta.9 version
 ```
 
 ## Source
@@ -101,7 +101,7 @@ $ kpt version
   https://console.cloud.google.com/gcr/images/kpt-dev/GLOBAL/kpt-gcloud?gcrImageListsize=30
 [cloud-sdk]: https://github.com/GoogleCloudPlatform/cloud-sdk-docker
 [linux]:
-  https://github.com/GoogleContainerTools/kpt/releases/download/v1.0.0-beta.7/kpt_linux_amd64
+  https://github.com/GoogleContainerTools/kpt/releases/download/v1.0.0-beta.9/kpt_linux_amd64
 [darwin]:
-  https://github.com/GoogleContainerTools/kpt/releases/download/v1.0.0-beta.7/kpt_darwin_amd64
+  https://github.com/GoogleContainerTools/kpt/releases/download/v1.0.0-beta.9/kpt_darwin_amd64
 [migration guide]: /installation/migration
diff --git a/site/reference/cli/fn/eval/README.md b/site/reference/cli/fn/eval/README.md
index d8c2e27d..287f5c0f 100644
--- a/site/reference/cli/fn/eval/README.md
+++ b/site/reference/cli/fn/eval/README.md
@@ -90,7 +90,7 @@ fn-args:
   Container image of the function to execute e.g. `gcr.io/kpt-fn/set-namespace:v0.1`.
   For convenience, if full image path is not specified, `gcr.io/kpt-fn/` is added as default prefix.
   e.g. instead of passing `gcr.io/kpt-fn/set-namespace:v0.1` you can pass `set-namespace:v0.1`.
-  `eval` executes only one function, so do not use `--exec-path` flag with this flag.
+  `eval` executes only one function, so do not use `--exec` flag with this flag.
 
 --image-pull-policy:
   If the image should be pulled before rendering the package(s). It can be set
diff --git a/thirdparty/cmdconfig/commands/cmdsource/cmdsource.go b/thirdparty/cmdconfig/commands/cmdsource/cmdsource.go
index 52603b5c..16cab807 100644
--- a/thirdparty/cmdconfig/commands/cmdsource/cmdsource.go
+++ b/thirdparty/cmdconfig/commands/cmdsource/cmdsource.go
@@ -127,7 +127,7 @@ func (r *SourceRunner) runE(c *cobra.Command, args []string) error {
 			Writer:         printer.FromContextOrDie(r.Ctx).OutStream(),
 			FunctionConfig: functionConfig,
 			ClearAnnotations: []string{kioutil.IndexAnnotation, kioutil.PathAnnotation,
-				kioutil.LegacyPathAnnotation, kioutil.LegacyIndexAnnotation},
+				kioutil.LegacyPathAnnotation, kioutil.LegacyIndexAnnotation}, // nolint:staticcheck
 		})
 	}
 


Test patch:
diff --git a/e2e/live_test.go b/e2e/live_test.go
index 1ab91a6c..159c1e14 100644
--- a/e2e/live_test.go
+++ b/e2e/live_test.go
@@ -17,12 +17,12 @@
 package e2e
 
 import (
-	"fmt"
 	"os"
 	"path/filepath"
 	"testing"
 
 	livetest "github.com/GoogleContainerTools/kpt/pkg/test/live"
+	"github.com/stretchr/testify/require"
 )
 
 func TestLiveApply(t *testing.T) {
@@ -32,18 +32,40 @@ func TestLiveApply(t *testing.T) {
 func runTests(t *testing.T, path string) {
 	testCases := scanTestCases(t, path)
 
+	livetest.RemoveKindCluster(t)
+	livetest.CreateKindCluster(t)
+
 	for p := range testCases {
+		p := p
 		c := testCases[p]
-		for format := range c.Output {
-			testName := fmt.Sprintf("%s#%s", p, format)
-			t.Run(testName, func(t *testing.T) {
-				(&livetest.Runner{
-					Config: c,
-					Path: p,
-					Format: format,
-				}).Run(t)
-			})
+
+		if !c.Parallel {
+			continue
 		}
+
+		t.Run(p, func(t *testing.T) {
+			if c.Parallel {
+				t.Parallel()
+			}
+
+			if c.NoResourceGroup {
+				require.False(t, c.Parallel, "Parallel tests can not modify the RG CRD")
+				if livetest.CheckIfResourceGroupInstalled(t) {
+					livetest.RemoveResourceGroup(t)
+				}
+			} else {
+				livetest.InstallResourceGroup(t)
+			}
+
+			ns := filepath.Base(p)
+			livetest.CreateNamespace(t, ns)
+			defer livetest.RemoveNamespace(t, ns)
+
+			(&livetest.Runner{
+				Config:    c,
+				Path:      p,
+			}).Run(t)
+		})
 	}
 }
 
@@ -68,4 +90,4 @@ func scanTestCases(t *testing.T, path string) map[string]livetest.TestCaseConfig
 		t.Fatalf("failed to scan for test cases in %s", path)
 	}
 	return testCases
-}
\ No newline at end of file
+}
diff --git a/e2e/testdata/fn-eval/error-in-pipe/.expected/config.yaml b/e2e/testdata/fn-eval/error-in-pipe/.expected/config.yaml
index 1fe965f6..21480468 100644
--- a/e2e/testdata/fn-eval/error-in-pipe/.expected/config.yaml
+++ b/e2e/testdata/fn-eval/error-in-pipe/.expected/config.yaml
@@ -17,7 +17,7 @@ stdErr: |
   [RUNNING] "gcr.io/kpt-fn/set-namespace:v0.1.3"
   [FAIL] "gcr.io/kpt-fn/set-namespace:v0.1.3" in 0s
     Results:
-      [ERROR] failed to configure function: input namespace cannot be empty
+      [error]: failed to configure function: input namespace cannot be empty
     Stderr:
       "[error] /// : failed to configure function: input namespace cannot be empty"
     Exit code: 1
diff --git a/e2e/testdata/fn-eval/modify-legacy-path-annotation/.expected/config.yaml b/e2e/testdata/fn-eval/modify-legacy-path-annotation/.expected/config.yaml
new file mode 100644
index 00000000..a93746a3
--- /dev/null
+++ b/e2e/testdata/fn-eval/modify-legacy-path-annotation/.expected/config.yaml
@@ -0,0 +1,17 @@
+# Copyright 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+testType: eval
+image: gcr.io/kpt-fn/starlark:v0.3
+fnConfig: ../starlark-fn.yaml
diff --git a/e2e/testdata/fn-eval/modify-legacy-path-annotation/.expected/diff.patch b/e2e/testdata/fn-eval/modify-legacy-path-annotation/.expected/diff.patch
new file mode 100644
index 00000000..532465c2
--- /dev/null
+++ b/e2e/testdata/fn-eval/modify-legacy-path-annotation/.expected/diff.patch
@@ -0,0 +1,4 @@
+diff --git a/deployment.yaml b/newfilename.yaml
+similarity index 100%
+rename from deployment.yaml
+rename to newfilename.yaml
diff --git a/e2e/testdata/fn-eval/modify-legacy-path-annotation/.krmignore b/e2e/testdata/fn-eval/modify-legacy-path-annotation/.krmignore
new file mode 100644
index 00000000..9d7a4007
--- /dev/null
+++ b/e2e/testdata/fn-eval/modify-legacy-path-annotation/.krmignore
@@ -0,0 +1,1 @@
+.expected
diff --git a/e2e/testdata/fn-eval/modify-legacy-path-annotation/deployment.yaml b/e2e/testdata/fn-eval/modify-legacy-path-annotation/deployment.yaml
new file mode 100644
index 00000000..f79e0a8b
--- /dev/null
+++ b/e2e/testdata/fn-eval/modify-legacy-path-annotation/deployment.yaml
@@ -0,0 +1,35 @@
+# 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:
+  labels:
+    foo: bar
+  name: my-nginx
+spec:
+  replicas: 3
+  selector:
+    matchLabels:
+      app: nginx
+  template:
+    metadata:
+      labels:
+        app: nginx
+    spec:
+      containers:
+        - image: nginx:1.14.2
+          name: nginx
+          ports:
+            - containerPort: 80
+              protocol: TCP
diff --git a/e2e/testdata/fn-eval/modify-legacy-path-annotation/starlark-fn.yaml b/e2e/testdata/fn-eval/modify-legacy-path-annotation/starlark-fn.yaml
new file mode 100644
index 00000000..a56ce8aa
--- /dev/null
+++ b/e2e/testdata/fn-eval/modify-legacy-path-annotation/starlark-fn.yaml
@@ -0,0 +1,21 @@
+# Copyright 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+apiVersion: fn.kpt.dev/v1alpha1
+kind: StarlarkRun
+metadata:
+  name: update-path
+source: |
+  for r in ctx.resource_list["items"]:
+    if r["kind"] == "Deployment":
+      r["metadata"]["annotations"]["config.kubernetes.io/path"] = "newfilename.yaml"
diff --git a/e2e/testdata/fn-eval/modify-path-annotation/.expected/config.yaml b/e2e/testdata/fn-eval/modify-path-annotation/.expected/config.yaml
new file mode 100644
index 00000000..a93746a3
--- /dev/null
+++ b/e2e/testdata/fn-eval/modify-path-annotation/.expected/config.yaml
@@ -0,0 +1,17 @@
+# Copyright 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+testType: eval
+image: gcr.io/kpt-fn/starlark:v0.3
+fnConfig: ../starlark-fn.yaml
diff --git a/e2e/testdata/fn-eval/modify-path-annotation/.expected/diff.patch b/e2e/testdata/fn-eval/modify-path-annotation/.expected/diff.patch
new file mode 100644
index 00000000..532465c2
--- /dev/null
+++ b/e2e/testdata/fn-eval/modify-path-annotation/.expected/diff.patch
@@ -0,0 +1,4 @@
+diff --git a/deployment.yaml b/newfilename.yaml
+similarity index 100%
+rename from deployment.yaml
+rename to newfilename.yaml
diff --git a/e2e/testdata/fn-eval/modify-path-annotation/.krmignore b/e2e/testdata/fn-eval/modify-path-annotation/.krmignore
new file mode 100644
index 00000000..9d7a4007
--- /dev/null
+++ b/e2e/testdata/fn-eval/modify-path-annotation/.krmignore
@@ -0,0 +1,1 @@
+.expected
diff --git a/e2e/testdata/fn-eval/modify-path-annotation/deployment.yaml b/e2e/testdata/fn-eval/modify-path-annotation/deployment.yaml
new file mode 100644
index 00000000..f79e0a8b
--- /dev/null
+++ b/e2e/testdata/fn-eval/modify-path-annotation/deployment.yaml
@@ -0,0 +1,35 @@
+# 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:
+  labels:
+    foo: bar
+  name: my-nginx
+spec:
+  replicas: 3
+  selector:
+    matchLabels:
+      app: nginx
+  template:
+    metadata:
+      labels:
+        app: nginx
+    spec:
+      containers:
+        - image: nginx:1.14.2
+          name: nginx
+          ports:
+            - containerPort: 80
+              protocol: TCP
diff --git a/e2e/testdata/fn-eval/modify-path-annotation/starlark-fn.yaml b/e2e/testdata/fn-eval/modify-path-annotation/starlark-fn.yaml
new file mode 100644
index 00000000..dae32d9d
--- /dev/null
+++ b/e2e/testdata/fn-eval/modify-path-annotation/starlark-fn.yaml
@@ -0,0 +1,21 @@
+# Copyright 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+apiVersion: fn.kpt.dev/v1alpha1
+kind: StarlarkRun
+metadata:
+  name: update-path
+source: |
+  for r in ctx.resource_list["items"]:
+    if r["kind"] == "Deployment":
+      r["metadata"]["annotations"]["internal.config.kubernetes.io/path"] = "newfilename.yaml"
diff --git a/e2e/testdata/fn-eval/out-of-place-fnchain-stdout/.expected/config.yaml b/e2e/testdata/fn-eval/out-of-place-fnchain-stdout/.expected/config.yaml
index 47dbb055..8ba1990f 100644
--- a/e2e/testdata/fn-eval/out-of-place-fnchain-stdout/.expected/config.yaml
+++ b/e2e/testdata/fn-eval/out-of-place-fnchain-stdout/.expected/config.yaml
@@ -6,7 +6,7 @@ stdErr: |
   [RUNNING] "gcr.io/kpt-fn/set-labels:v0.1.3"
   [PASS] "gcr.io/kpt-fn/set-labels:v0.1.3" in 0s
 stdOut: |
-  apiVersion: config.kubernetes.io/v1alpha1
+  apiVersion: config.kubernetes.io/v1
   kind: ResourceList
   items:
   # Copyright 2021 Google LLC
diff --git a/e2e/testdata/fn-eval/output-to-stdout/.expected/config.yaml b/e2e/testdata/fn-eval/output-to-stdout/.expected/config.yaml
index 3c6be816..79bfb94b 100644
--- a/e2e/testdata/fn-eval/output-to-stdout/.expected/config.yaml
+++ b/e2e/testdata/fn-eval/output-to-stdout/.expected/config.yaml
@@ -13,7 +13,7 @@
 # limitations under the License.
 
 stdOut: |
-  apiVersion: config.kubernetes.io/v1alpha1
+  apiVersion: config.kubernetes.io/v1
   kind: ResourceList
   items:
   - # Copyright 2021 Google LLC
diff --git a/e2e/testdata/fn-eval/structured-results-in-stdout/.expected/config.yaml b/e2e/testdata/fn-eval/structured-results-in-stdout/.expected/config.yaml
index 72b28254..c47fc659 100644
--- a/e2e/testdata/fn-eval/structured-results-in-stdout/.expected/config.yaml
+++ b/e2e/testdata/fn-eval/structured-results-in-stdout/.expected/config.yaml
@@ -20,4 +20,4 @@ testType: eval
 image: gcr.io/kpt-fn/search-replace:v0.1
 fnConfig: ../search-replace-conf.yaml
 # Non-error messages should be printed to stderr
-stdErr: "[INFO]"
+stdErr: "[info]"
diff --git a/e2e/testdata/fn-render/fn-failure-output-no-truncate/.expected/config.yaml b/e2e/testdata/fn-render/fn-failure-output-no-truncate/.expected/config.yaml
index 39ff35a6..805fda8c 100644
--- a/e2e/testdata/fn-render/fn-failure-output-no-truncate/.expected/config.yaml
+++ b/e2e/testdata/fn-render/fn-failure-output-no-truncate/.expected/config.yaml
@@ -16,4 +16,4 @@
 # non-zero exit code and no changes in the resources.
 exitCode: 1
 disableOutputTruncate: true
-stdErr: '[ERROR] Invalid type. Expected: [integer,null], given: string in object "apps/v1/Deployment/nginx-deployment" in file "resources.yaml" in field "spec.replicas"'
+stdErr: '[error] apps/v1/Deployment/nginx-deployment spec.replicas: Invalid type. Expected: [integer,null], given: string'
diff --git a/e2e/testdata/fn-render/fn-failure/.expected/config.yaml b/e2e/testdata/fn-render/fn-failure/.expected/config.yaml
index cd8c3db9..0da7f168 100644
--- a/e2e/testdata/fn-render/fn-failure/.expected/config.yaml
+++ b/e2e/testdata/fn-render/fn-failure/.expected/config.yaml
@@ -19,6 +19,6 @@ stdErr: |
   [RUNNING] "gcr.io/kpt-fn/starlark:v0.2.1"
   [FAIL] "gcr.io/kpt-fn/starlark:v0.2.1" in 0s
     Results:
-      [ERROR] httpbin-gen:4:1: got newline, want primary expression
+      [error]: httpbin-gen:4:1: got newline, want primary expression
     Stderr:
       "[error] : httpbin-gen:4:1: got newline, want primary expression"
diff --git a/e2e/testdata/fn-render/fnresult-fn-failure/.expected/config.yaml b/e2e/testdata/fn-render/fnresult-fn-failure/.expected/config.yaml
index f62684eb..7c9cbaf3 100644
--- a/e2e/testdata/fn-render/fnresult-fn-failure/.expected/config.yaml
+++ b/e2e/testdata/fn-render/fnresult-fn-failure/.expected/config.yaml
@@ -15,4 +15,4 @@
 # One of the functions in the pipeline fails resulting in
 # non-zero exit code and no changes in the resources.
 exitCode: 1
-stdErr: '[ERROR] Invalid type. Expected: [integer,null], given: string in object "apps/v1/Deployment/nginx-deployment" in file "resources.yaml" in field "spec.replicas"'
+stdErr: '[error] apps/v1/Deployment/nginx-deployment spec.replicas: Invalid type. Expected: [integer,null], given: string'
diff --git a/e2e/testdata/fn-render/kubeval-failure/.expected/config.yaml b/e2e/testdata/fn-render/kubeval-failure/.expected/config.yaml
index c8925faf..b257d428 100644
--- a/e2e/testdata/fn-render/kubeval-failure/.expected/config.yaml
+++ b/e2e/testdata/fn-render/kubeval-failure/.expected/config.yaml
@@ -15,4 +15,3 @@
 # One of the functions in the pipeline fails resulting in
 # non-zero exit code and no changes in the resources.
 exitCode: 1
-stdErr: '[ERROR] selector is required in object "apps/v1/Deployment/nginx-deployment" in file "resources.yaml" in field "selector"'
diff --git a/e2e/testdata/fn-render/modify-legacy-path-annotation/.expected/diff.patch b/e2e/testdata/fn-render/modify-legacy-path-annotation/.expected/diff.patch
new file mode 100644
index 00000000..532465c2
--- /dev/null
+++ b/e2e/testdata/fn-render/modify-legacy-path-annotation/.expected/diff.patch
@@ -0,0 +1,4 @@
+diff --git a/deployment.yaml b/newfilename.yaml
+similarity index 100%
+rename from deployment.yaml
+rename to newfilename.yaml
diff --git a/e2e/testdata/fn-render/modify-legacy-path-annotation/.krmignore b/e2e/testdata/fn-render/modify-legacy-path-annotation/.krmignore
new file mode 100644
index 00000000..9d7a4007
--- /dev/null
+++ b/e2e/testdata/fn-render/modify-legacy-path-annotation/.krmignore
@@ -0,0 +1,1 @@
+.expected
diff --git a/e2e/testdata/fn-render/modify-legacy-path-annotation/Kptfile b/e2e/testdata/fn-render/modify-legacy-path-annotation/Kptfile
new file mode 100644
index 00000000..81bbec7a
--- /dev/null
+++ b/e2e/testdata/fn-render/modify-legacy-path-annotation/Kptfile
@@ -0,0 +1,8 @@
+apiVersion: kpt.dev/v1
+kind: Kptfile
+metadata:
+  name: app
+pipeline:
+  mutators:
+  - image: gcr.io/kpt-fn/starlark:v0.3.0
+    configPath: starlark-fn.yaml
diff --git a/e2e/testdata/fn-render/modify-legacy-path-annotation/deployment.yaml b/e2e/testdata/fn-render/modify-legacy-path-annotation/deployment.yaml
new file mode 100644
index 00000000..6590f6ca
--- /dev/null
+++ b/e2e/testdata/fn-render/modify-legacy-path-annotation/deployment.yaml
@@ -0,0 +1,19 @@
+# Copyright 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: nginx-deployment
+spec:
+  replicas: 3
diff --git a/e2e/testdata/fn-render/modify-legacy-path-annotation/starlark-fn.yaml b/e2e/testdata/fn-render/modify-legacy-path-annotation/starlark-fn.yaml
new file mode 100644
index 00000000..f4393c29
--- /dev/null
+++ b/e2e/testdata/fn-render/modify-legacy-path-annotation/starlark-fn.yaml
@@ -0,0 +1,22 @@
+# 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: update-path
+source: |
+  for r in ctx.resource_list["items"]:
+    if r["kind"] == "Deployment":
+      r["metadata"]["annotations"]["config.kubernetes.io/path"] = "newfilename.yaml"
diff --git a/e2e/testdata/fn-render/modify-path-annotation/.expected/diff.patch b/e2e/testdata/fn-render/modify-path-annotation/.expected/diff.patch
new file mode 100644
index 00000000..532465c2
--- /dev/null
+++ b/e2e/testdata/fn-render/modify-path-annotation/.expected/diff.patch
@@ -0,0 +1,4 @@
+diff --git a/deployment.yaml b/newfilename.yaml
+similarity index 100%
+rename from deployment.yaml
+rename to newfilename.yaml
diff --git a/e2e/testdata/fn-render/modify-path-annotation/.krmignore b/e2e/testdata/fn-render/modify-path-annotation/.krmignore
new file mode 100644
index 00000000..9d7a4007
--- /dev/null
+++ b/e2e/testdata/fn-render/modify-path-annotation/.krmignore
@@ -0,0 +1,1 @@
+.expected
diff --git a/e2e/testdata/fn-render/modify-path-annotation/Kptfile b/e2e/testdata/fn-render/modify-path-annotation/Kptfile
new file mode 100644
index 00000000..81bbec7a
--- /dev/null
+++ b/e2e/testdata/fn-render/modify-path-annotation/Kptfile
@@ -0,0 +1,8 @@
+apiVersion: kpt.dev/v1
+kind: Kptfile
+metadata:
+  name: app
+pipeline:
+  mutators:
+  - image: gcr.io/kpt-fn/starlark:v0.3.0
+    configPath: starlark-fn.yaml
diff --git a/e2e/testdata/fn-render/modify-path-annotation/deployment.yaml b/e2e/testdata/fn-render/modify-path-annotation/deployment.yaml
new file mode 100644
index 00000000..6590f6ca
--- /dev/null
+++ b/e2e/testdata/fn-render/modify-path-annotation/deployment.yaml
@@ -0,0 +1,19 @@
+# Copyright 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: nginx-deployment
+spec:
+  replicas: 3
diff --git a/e2e/testdata/fn-render/modify-path-annotation/starlark-fn.yaml b/e2e/testdata/fn-render/modify-path-annotation/starlark-fn.yaml
new file mode 100644
index 00000000..1e786513
--- /dev/null
+++ b/e2e/testdata/fn-render/modify-path-annotation/starlark-fn.yaml
@@ -0,0 +1,22 @@
+# 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: update-path
+source: |
+  for r in ctx.resource_list["items"]:
+    if r["kind"] == "Deployment":
+      r["metadata"]["annotations"]["internal.config.kubernetes.io/path"] = "newfilename.yaml"
diff --git a/e2e/testdata/fn-render/out-of-place-fnchain-stdout-results/.expected/config.yaml b/e2e/testdata/fn-render/out-of-place-fnchain-stdout-results/.expected/config.yaml
index f99568bd..2ce82921 100644
--- a/e2e/testdata/fn-render/out-of-place-fnchain-stdout-results/.expected/config.yaml
+++ b/e2e/testdata/fn-render/out-of-place-fnchain-stdout-results/.expected/config.yaml
@@ -1,5 +1,5 @@
 stdOut: |
-  apiVersion: config.kubernetes.io/v1alpha1
+  apiVersion: config.kubernetes.io/v1
   kind: ResourceList
   items:
   # Copyright 2021 Google LLC
diff --git a/e2e/testdata/fn-render/out-of-place-fnchain-stdout/.expected/config.yaml b/e2e/testdata/fn-render/out-of-place-fnchain-stdout/.expected/config.yaml
index aa1d01fe..6d080b0c 100644
--- a/e2e/testdata/fn-render/out-of-place-fnchain-stdout/.expected/config.yaml
+++ b/e2e/testdata/fn-render/out-of-place-fnchain-stdout/.expected/config.yaml
@@ -8,7 +8,7 @@ stdErr: |
   [PASS] "gcr.io/kpt-fn/set-labels:v0.1.3" in 0s
 
 stdOut: |
-  apiVersion: config.kubernetes.io/v1alpha1
+  apiVersion: config.kubernetes.io/v1
   kind: ResourceList
   items:
   # Copyright 2021 Google LLC
diff --git a/e2e/testdata/fn-render/out-of-place-stdout/.expected/config.yaml b/e2e/testdata/fn-render/out-of-place-stdout/.expected/config.yaml
index 51059413..9e516bdc 100644
--- a/e2e/testdata/fn-render/out-of-place-stdout/.expected/config.yaml
+++ b/e2e/testdata/fn-render/out-of-place-stdout/.expected/config.yaml
@@ -3,7 +3,7 @@ stdErr: |
   [PASS] "gcr.io/kpt-fn/set-namespace:v0.1.3" in 0s
 
 stdOut: |
-  apiVersion: config.kubernetes.io/v1alpha1
+  apiVersion: config.kubernetes.io/v1
   kind: ResourceList
   items:
   # Copyright 2021 Google LLC
diff --git a/e2e/testdata/fn-source/subpkgs-with-krmignore/.expected/config.yaml b/e2e/testdata/fn-source/subpkgs-with-krmignore/.expected/config.yaml
index 1d55f23d..79b950dd 100644
--- a/e2e/testdata/fn-source/subpkgs-with-krmignore/.expected/config.yaml
+++ b/e2e/testdata/fn-source/subpkgs-with-krmignore/.expected/config.yaml
@@ -1,5 +1,5 @@
 stdOut: |-
-  apiVersion: config.kubernetes.io/v1alpha1
+  apiVersion: config.kubernetes.io/v1
   kind: ResourceList
   items:
   - # Copyright 2021 Google LLC
diff --git a/e2e/testdata/live-apply/apply-depends-on/config.yaml b/e2e/testdata/live-apply/apply-depends-on/config.yaml
index 51421563..c12c7ab5 100644
--- a/e2e/testdata/live-apply/apply-depends-on/config.yaml
+++ b/e2e/testdata/live-apply/apply-depends-on/config.yaml
@@ -12,20 +12,17 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-preinstallResourceGroup: true
-output:
-  events:
-    stdOut: |
-      deployment.apps/first-nginx created
-      1 resource(s) applied. 1 created, 0 unchanged, 0 configured, 0 failed
-      deployment.apps/second-nginx created
-      1 resource(s) applied. 1 created, 0 unchanged, 0 configured, 0 failed
-  json:
-    stdOut: |
-      {"eventType":"resourceApplied","group":"apps","kind":"Deployment","name":"first-nginx","namespace":"apply-depends-on","operation":"Created","timestamp":"<TIMESTAMP>","type":"apply"}
-      {"configuredCount":0,"count":1,"createdCount":1,"eventType":"completed","failedCount":0,"serverSideCount":0,"timestamp":"<TIMESTAMP>","type":"apply","unchangedCount":0}
-      {"eventType":"resourceApplied","group":"apps","kind":"Deployment","name":"second-nginx","namespace":"apply-depends-on","operation":"Created","timestamp":"<TIMESTAMP>","type":"apply"}
-      {"configuredCount":0,"count":1,"createdCount":1,"eventType":"completed","failedCount":0,"serverSideCount":0,"timestamp":"<TIMESTAMP>","type":"apply","unchangedCount":0}
+parallel: true
+
+kptArgs:
+  - "--reconcile-timeout=1m"
+
+stdOut: |
+  deployment.apps/first-nginx created
+  1 resource(s) applied. 1 created, 0 unchanged, 0 configured, 0 failed
+  deployment.apps/second-nginx created
+  1 resource(s) applied. 1 created, 0 unchanged, 0 configured, 0 failed
+
 inventory:
   - group: apps
     kind: Deployment
diff --git a/e2e/testdata/live-apply/apply-depends-on/resources/first.yaml b/e2e/testdata/live-apply/apply-depends-on/resources/first.yaml
index 00bbf522..b5a95d23 100644
--- a/e2e/testdata/live-apply/apply-depends-on/resources/first.yaml
+++ b/e2e/testdata/live-apply/apply-depends-on/resources/first.yaml
@@ -18,7 +18,7 @@ metadata:
   name: first-nginx
   namespace: apply-depends-on
 spec:
-  replicas: 2
+  replicas: 1
   selector:
     matchLabels:
       app: first-nginx
diff --git a/e2e/testdata/live-apply/apply-depends-on/resources/second.yaml b/e2e/testdata/live-apply/apply-depends-on/resources/second.yaml
index 64e5dcc6..f3dfb252 100644
--- a/e2e/testdata/live-apply/apply-depends-on/resources/second.yaml
+++ b/e2e/testdata/live-apply/apply-depends-on/resources/second.yaml
@@ -20,7 +20,7 @@ metadata:
   annotations:
     config.kubernetes.io/depends-on: apps/namespaces/apply-depends-on/Deployment/first-nginx
 spec:
-  replicas: 2
+  replicas: 1
   selector:
     matchLabels:
       app: second-nginx
diff --git a/e2e/testdata/live-apply/crd-and-cr/config.yaml b/e2e/testdata/live-apply/crd-and-cr/config.yaml
index 4db0e064..943b973a 100644
--- a/e2e/testdata/live-apply/crd-and-cr/config.yaml
+++ b/e2e/testdata/live-apply/crd-and-cr/config.yaml
@@ -12,21 +12,16 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-requiresCleanCluster: true
-preinstallResourceGroup: true
-output:
-  events:
-    stdOut: |
-      customresourcedefinition.apiextensions.k8s.io/customs.kpt.dev created
-      1 resource(s) applied. 1 created, 0 unchanged, 0 configured, 0 failed
-      custom.kpt.dev/cr created
-      1 resource(s) applied. 1 created, 0 unchanged, 0 configured, 0 failed
-  json:
-    stdOut: |
-      {"eventType":"resourceApplied","group":"apiextensions.k8s.io","kind":"CustomResourceDefinition","name":"customs.kpt.dev","namespace":"","operation":"Created","timestamp":"<TIMESTAMP>","type":"apply"}
-      {"configuredCount":0,"count":1,"createdCount":1,"eventType":"completed","failedCount":0,"serverSideCount":0,"timestamp":"<TIMESTAMP>","type":"apply","unchangedCount":0}
-      {"eventType":"resourceApplied","group":"kpt.dev","kind":"Custom","name":"cr","namespace":"","operation":"Created","timestamp":"<TIMESTAMP>","type":"apply"}
-      {"configuredCount":0,"count":1,"createdCount":1,"eventType":"completed","failedCount":0,"serverSideCount":0,"timestamp":"<TIMESTAMP>","type":"apply","unchangedCount":0}
+parallel: true
+
+kptArgs:
+  - "--reconcile-timeout=1m"
+
+stdOut: |
+  customresourcedefinition.apiextensions.k8s.io/customs.kpt.dev created
+  1 resource(s) applied. 1 created, 0 unchanged, 0 configured, 0 failed
+  custom.kpt.dev/cr created
+  1 resource(s) applied. 1 created, 0 unchanged, 0 configured, 0 failed
 inventory:
   - group: apiextensions.k8s.io
     kind: CustomResourceDefinition
diff --git a/e2e/testdata/live-apply/dry-run-with-install-rg/config.yaml b/e2e/testdata/live-apply/dry-run-with-install-rg/config.yaml
index 7f10c3e8..5f8ab9af 100644
--- a/e2e/testdata/live-apply/dry-run-with-install-rg/config.yaml
+++ b/e2e/testdata/live-apply/dry-run-with-install-rg/config.yaml
@@ -12,21 +12,14 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-requiresCleanCluster: true
+parallel: false
+noResourceGroup: true
 kptArgs:
   - "--dry-run"
   - "--install-resource-group"
-output:
-  events:
-    stdOut: |
-      deployment.apps/nginx-deployment created (dry-run)
-      1 resource(s) applied. 1 created, 0 unchanged, 0 configured, 0 failed (dry-run)
-    stdErr: |
-      installing inventory ResourceGroup CRD.
-  json:
-    stdOut: |
-      {"eventType":"resourceApplied","group":"apps","kind":"Deployment","name":"nginx-deployment","namespace":"dry-run-with-install-rg","operation":"Created","timestamp":"<TIMESTAMP>","type":"apply"}
-      {"configuredCount":0,"count":1,"createdCount":1,"eventType":"completed","failedCount":0,"serverSideCount":0,"timestamp":"<TIMESTAMP>","type":"apply","unchangedCount":0}
-    stdErr: |
-      installing inventory ResourceGroup CRD.
+stdOut: |
+  deployment.apps/nginx-deployment created (dry-run)
+  1 resource(s) applied. 1 created, 0 unchanged, 0 configured, 0 failed (dry-run)
+stdErr: |
+  installing inventory ResourceGroup CRD.
 exitCode: 0
\ No newline at end of file
diff --git a/e2e/testdata/live-apply/dry-run-with-install-rg/resources/deployment.yaml b/e2e/testdata/live-apply/dry-run-with-install-rg/resources/deployment.yaml
index d37e0895..38c79d76 100644
--- a/e2e/testdata/live-apply/dry-run-with-install-rg/resources/deployment.yaml
+++ b/e2e/testdata/live-apply/dry-run-with-install-rg/resources/deployment.yaml
@@ -24,7 +24,7 @@ spec:
     rollingUpdate:
       maxUnavailable: 1
       maxSurge: 0
-  replicas: 2
+  replicas: 1
   selector:
     matchLabels:
       app: nginx-deployment
diff --git a/e2e/testdata/live-apply/dry-run-without-rg/config.yaml b/e2e/testdata/live-apply/dry-run-without-rg/config.yaml
index b89bf9c9..608a7e83 100644
--- a/e2e/testdata/live-apply/dry-run-without-rg/config.yaml
+++ b/e2e/testdata/live-apply/dry-run-without-rg/config.yaml
@@ -12,11 +12,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-requiresCleanCluster: true
+parallel: false
+noResourceGroup: true
 kptArgs:
   - "--dry-run"
-output:
-  events:
-    stdErr: |
-      Error: The ResourceGroup CRD was not found in the cluster. Please install it either by using the '--install-resource-group' flag or the 'kpt live install-resource-group' command.
+stdErr: |
+  Error: The ResourceGroup CRD was not found in the cluster. Please install it either by using the '--install-resource-group' flag or the 'kpt live install-resource-group' command.
 exitCode: 1
\ No newline at end of file
diff --git a/e2e/testdata/live-apply/install-rg-on-apply/config.yaml b/e2e/testdata/live-apply/install-rg-on-apply/config.yaml
index 207b4a45..b0019b96 100644
--- a/e2e/testdata/live-apply/install-rg-on-apply/config.yaml
+++ b/e2e/testdata/live-apply/install-rg-on-apply/config.yaml
@@ -12,20 +12,15 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-requiresCleanCluster: true
-output:
-  events:
-    stdOut: |
-      deployment.apps/nginx-deployment created
-      1 resource(s) applied. 1 created, 0 unchanged, 0 configured, 0 failed
-    stdErr: |
-      installing inventory ResourceGroup CRD.
-  json:
-    stdOut: |
-      {"eventType":"resourceApplied","group":"apps","kind":"Deployment","name":"nginx-deployment","namespace":"install-rg-on-apply","operation":"Created","timestamp":"<TIMESTAMP>","type":"apply"}
-      {"configuredCount":0,"count":1,"createdCount":1,"eventType":"completed","failedCount":0,"serverSideCount":0,"timestamp":"<TIMESTAMP>","type":"apply","unchangedCount":0}
-    stdErr: |
-      installing inventory ResourceGroup CRD.
+parallel: false
+noResourceGroup: true
+kptArgs:
+  - "--reconcile-timeout=1m"
+stdOut: |
+  deployment.apps/nginx-deployment created
+  1 resource(s) applied. 1 created, 0 unchanged, 0 configured, 0 failed
+stdErr: |
+  installing inventory ResourceGroup CRD.
 inventory:
   - group: apps
     kind: Deployment
diff --git a/e2e/testdata/live-apply/install-rg-on-apply/resources/deployment.yaml b/e2e/testdata/live-apply/install-rg-on-apply/resources/deployment.yaml
index 247e4fd6..1b521f24 100644
--- a/e2e/testdata/live-apply/install-rg-on-apply/resources/deployment.yaml
+++ b/e2e/testdata/live-apply/install-rg-on-apply/resources/deployment.yaml
@@ -24,7 +24,7 @@ spec:
     rollingUpdate:
       maxUnavailable: 1
       maxSurge: 0
-  replicas: 2
+  replicas: 1
   selector:
     matchLabels:
       app: nginx-deployment
diff --git a/e2e/testdata/live-apply/json-output/config.yaml b/e2e/testdata/live-apply/json-output/config.yaml
new file mode 100644
index 00000000..1d950b5f
--- /dev/null
+++ b/e2e/testdata/live-apply/json-output/config.yaml
@@ -0,0 +1,35 @@
+# 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.
+
+parallel: true
+kptArgs:
+  - "--output=json"
+  - "--reconcile-timeout=1m"
+stdOut: |
+  {"eventType":"resourceApplied","group":"","kind":"ConfigMap","name":"cm","namespace":"json-output","operation":"Created","timestamp":"<TIMESTAMP>","type":"apply"}
+  {"configuredCount":0,"count":1,"createdCount":1,"eventType":"completed","failedCount":0,"serverSideCount":0,"timestamp":"<TIMESTAMP>","type":"apply","unchangedCount":0}
+  {"eventType":"resourceApplied","group":"apps","kind":"Deployment","name":"nginx","namespace":"json-output","operation":"Created","timestamp":"<TIMESTAMP>","type":"apply"}
+  {"configuredCount":0,"count":1,"createdCount":1,"eventType":"completed","failedCount":0,"serverSideCount":0,"timestamp":"<TIMESTAMP>","type":"apply","unchangedCount":0}
+  {"eventType":"resourcePruned","group":"apps","kind":"Deployment","name":"second-nginx","namespace":"json-output","operation":"Pruned","timestamp":"<TIMESTAMP>","type":"prune"}
+  {"eventType":"completed","pruned":1,"skipped":0,"timestamp":"<TIMESTAMP>","type":"prune"}
+  {"eventType":"resourcePruned","group":"apps","kind":"Deployment","name":"first-nginx","namespace":"json-output","operation":"Pruned","timestamp":"<TIMESTAMP>","type":"prune"}
+  {"eventType":"completed","pruned":1,"skipped":0,"timestamp":"<TIMESTAMP>","type":"prune"}
+inventory:
+  - kind: ConfigMap
+    name: cm
+    namespace: json-output
+  - group: apps
+    kind: Deployment
+    name: nginx
+    namespace: json-output
\ No newline at end of file
diff --git a/e2e/testdata/live-apply/json-output/pre-apply/first.yaml b/e2e/testdata/live-apply/json-output/pre-apply/first.yaml
new file mode 100644
index 00000000..f671e0b9
--- /dev/null
+++ b/e2e/testdata/live-apply/json-output/pre-apply/first.yaml
@@ -0,0 +1,36 @@
+# 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: first-nginx
+  namespace: json-output
+  annotations:
+    config.k8s.io/owning-inventory: json-output
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: first-nginx
+  template:
+    metadata:
+      labels:
+        app: first-nginx
+    spec:
+      containers:
+        - name: nginx
+          image: nginx:1.14.2
+          ports:
+            - containerPort: 80
\ No newline at end of file
diff --git a/e2e/testdata/live-apply/json-output/pre-apply/rg.yaml b/e2e/testdata/live-apply/json-output/pre-apply/rg.yaml
new file mode 100644
index 00000000..506bc71b
--- /dev/null
+++ b/e2e/testdata/live-apply/json-output/pre-apply/rg.yaml
@@ -0,0 +1,31 @@
+# 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: kpt.dev/v1alpha1
+kind: ResourceGroup
+metadata:
+  labels:
+    cli-utils.sigs.k8s.io/inventory-id: json-output
+  name: json-output
+  namespace: json-output
+spec:
+  resources:
+    - group: apps
+      kind: Deployment
+      name: first-nginx
+      namespace: json-output
+    - group: apps
+      kind: Deployment
+      name: second-nginx
+      namespace: json-output
\ No newline at end of file
diff --git a/e2e/testdata/live-apply/json-output/pre-apply/second.yaml b/e2e/testdata/live-apply/json-output/pre-apply/second.yaml
new file mode 100644
index 00000000..3c4fdfa8
--- /dev/null
+++ b/e2e/testdata/live-apply/json-output/pre-apply/second.yaml
@@ -0,0 +1,37 @@
+# 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: second-nginx
+  namespace: json-output
+  annotations:
+    config.k8s.io/owning-inventory: json-output
+    config.kubernetes.io/depends-on: apps/namespaces/json-output/Deployment/first-nginx
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: second-nginx
+  template:
+    metadata:
+      labels:
+        app: second-nginx
+    spec:
+      containers:
+        - name: nginx
+          image: nginx:1.14.2
+          ports:
+            - containerPort: 80
\ No newline at end of file
diff --git a/e2e/testdata/live-apply/json-output/resources/Kptfile b/e2e/testdata/live-apply/json-output/resources/Kptfile
new file mode 100644
index 00000000..dff6968e
--- /dev/null
+++ b/e2e/testdata/live-apply/json-output/resources/Kptfile
@@ -0,0 +1,8 @@
+apiVersion: kpt.dev/v1
+kind: Kptfile
+metadata:
+  name: json-output
+inventory:
+  namespace: json-output
+  name: json-output
+  inventoryID: json-output
diff --git a/e2e/testdata/live-apply/json-output/resources/cm.yaml b/e2e/testdata/live-apply/json-output/resources/cm.yaml
new file mode 100644
index 00000000..153818fe
--- /dev/null
+++ b/e2e/testdata/live-apply/json-output/resources/cm.yaml
@@ -0,0 +1,21 @@
+# Copyright 2021 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: cm
+  namespace: json-output
+data:
+  foo: bar
\ No newline at end of file
diff --git a/e2e/testdata/live-apply/json-output/resources/dep.yaml b/e2e/testdata/live-apply/json-output/resources/dep.yaml
new file mode 100644
index 00000000..eb0129b4
--- /dev/null
+++ b/e2e/testdata/live-apply/json-output/resources/dep.yaml
@@ -0,0 +1,36 @@
+# 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
+  namespace: json-output
+  annotations:
+    config.kubernetes.io/depends-on: apps/namespaces/json-output/ConfigMap/cm
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: nginx
+  template:
+    metadata:
+      labels:
+        app: nginx
+    spec:
+      containers:
+        - name: nginx
+          image: nginx:1.14.2
+          ports:
+            - containerPort: 80
\ No newline at end of file
diff --git a/e2e/testdata/live-apply/prune-depends-on/config.yaml b/e2e/testdata/live-apply/prune-depends-on/config.yaml
index 6a96ac6a..91e57f40 100644
--- a/e2e/testdata/live-apply/prune-depends-on/config.yaml
+++ b/e2e/testdata/live-apply/prune-depends-on/config.yaml
@@ -12,24 +12,18 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-preinstallResourceGroup: true
-output:
-  events:
-    stdOut: |
-      configmap/cm created
-      1 resource(s) applied. 1 created, 0 unchanged, 0 configured, 0 failed
-      deployment.apps/second-nginx pruned
-      1 resource(s) pruned, 0 skipped, 0 failed
-      deployment.apps/first-nginx pruned
-      1 resource(s) pruned, 0 skipped, 0 failed
-  json:
-    stdOut: |
-      {"eventType":"resourceApplied","group":"","kind":"ConfigMap","name":"cm","namespace":"prune-depends-on","operation":"Created","timestamp":"<TIMESTAMP>","type":"apply"}
-      {"configuredCount":0,"count":1,"createdCount":1,"eventType":"completed","failedCount":0,"serverSideCount":0,"timestamp":"<TIMESTAMP>","type":"apply","unchangedCount":0}
-      {"eventType":"resourcePruned","group":"apps","kind":"Deployment","name":"second-nginx","namespace":"prune-depends-on","operation":"Pruned","timestamp":"<TIMESTAMP>","type":"prune"}
-      {"eventType":"completed","pruned":1,"skipped":0,"timestamp":"<TIMESTAMP>","type":"prune"}
-      {"eventType":"resourcePruned","group":"apps","kind":"Deployment","name":"first-nginx","namespace":"prune-depends-on","operation":"Pruned","timestamp":"<TIMESTAMP>","type":"prune"}
-      {"eventType":"completed","pruned":1,"skipped":0,"timestamp":"<TIMESTAMP>","type":"prune"}
+parallel: true
+
+kptArgs:
+  - "--reconcile-timeout=1m"
+
+stdOut: |
+  configmap/cm created
+  1 resource(s) applied. 1 created, 0 unchanged, 0 configured, 0 failed
+  deployment.apps/second-nginx pruned
+  1 resource(s) pruned, 0 skipped, 0 failed
+  deployment.apps/first-nginx pruned
+  1 resource(s) pruned, 0 skipped, 0 failed
 inventory:
   - kind: ConfigMap
     name: cm
diff --git a/e2e/testdata/live-apply/prune-depends-on/pre-apply/first.yaml b/e2e/testdata/live-apply/prune-depends-on/pre-apply/first.yaml
index 16c645d4..5097c0af 100644
--- a/e2e/testdata/live-apply/prune-depends-on/pre-apply/first.yaml
+++ b/e2e/testdata/live-apply/prune-depends-on/pre-apply/first.yaml
@@ -20,7 +20,7 @@ metadata:
   annotations:
     config.k8s.io/owning-inventory: prune-depends-on
 spec:
-  replicas: 2
+  replicas: 1
   selector:
     matchLabels:
       app: first-nginx
diff --git a/e2e/testdata/live-apply/prune-depends-on/pre-apply/second.yaml b/e2e/testdata/live-apply/prune-depends-on/pre-apply/second.yaml
index f592733d..238a51aa 100644
--- a/e2e/testdata/live-apply/prune-depends-on/pre-apply/second.yaml
+++ b/e2e/testdata/live-apply/prune-depends-on/pre-apply/second.yaml
@@ -21,7 +21,7 @@ metadata:
     config.k8s.io/owning-inventory: prune-depends-on
     config.kubernetes.io/depends-on: apps/namespaces/prune-depends-on/Deployment/first-nginx
 spec:
-  replicas: 2
+  replicas: 1
   selector:
     matchLabels:
       app: second-nginx
diff --git a/internal/fnruntime/runner_test.go b/internal/fnruntime/runner_test.go
index 9c7494e3..8c3e7494 100644
--- a/internal/fnruntime/runner_test.go
+++ b/internal/fnruntime/runner_test.go
@@ -567,7 +567,7 @@ file:
 		yml, err := yaml.Parse(tc.input)
 		assert.NoError(t, err)
 
-		result := &framework.ResultItem{}
+		result := &framework.Result{}
 		err = yaml.Unmarshal([]byte(tc.input), result)
 		assert.NoError(t, err)
 		assert.NoError(t, populateResourceRef(yml, result))
diff --git a/internal/util/attribution/attribution_test.go b/internal/util/attribution/attribution_test.go
new file mode 100644
index 00000000..6743cb7c
--- /dev/null
+++ b/internal/util/attribution/attribution_test.go
@@ -0,0 +1,243 @@
+// 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 attribution
+
+import (
+	"io/ioutil"
+	"os"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestUsageProcess(t *testing.T) {
+	var tests = []struct {
+		name     string
+		input    string
+		group    string
+		disable  bool
+		expected string
+		errMsg   string
+	}{
+		{
+			name: "Don't add to non-cnrm resource",
+			input: `
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: nginx-deployment
+  namespace: my-space
+spec:
+  replicas: 3
+ `,
+			expected: `
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: nginx-deployment
+  namespace: my-space
+spec:
+  replicas: 3
+ `,
+		},
+		{
+			name: "Create metrics annotation",
+			input: `
+apiVersion: compute.cnrm.cloud.google.com/v1beta1
+kind: ComputeSubnetwork
+metadata:
+  name: network-name-subnetwork
+ `,
+			group: "pkg",
+			expected: `apiVersion: compute.cnrm.cloud.google.com/v1beta1
+kind: ComputeSubnetwork
+metadata:
+  name: network-name-subnetwork
+  annotations:
+    cnrm.cloud.google.com/blueprint: 'kpt-pkg'
+ `,
+		},
+		{
+			name: "Create metrics annotation",
+			input: `
+apiVersion: compute.cnrm.cloud.google.com/v1beta1
+kind: ComputeSubnetwork
+metadata:
+  name: network-name-subnetwork
+ `,
+			group:   "pkg",
+			disable: true,
+			expected: `apiVersion: compute.cnrm.cloud.google.com/v1beta1
+kind: ComputeSubnetwork
+metadata:
+  name: network-name-subnetwork
+ `,
+		},
+		{
+			name: "Add new group to existing metrics annotation",
+			input: `
+apiVersion: compute.cnrm.cloud.google.com/v1beta1
+kind: ComputeSubnetwork
+metadata:
+  name: network-name-subnetwork
+  annotations:
+    cnrm.cloud.google.com/blueprint: 'kpt-pkg'
+ `,
+			group: "fn",
+			expected: `apiVersion: compute.cnrm.cloud.google.com/v1beta1
+kind: ComputeSubnetwork
+metadata:
+  name: network-name-subnetwork
+  annotations:
+    cnrm.cloud.google.com/blueprint: 'kpt-pkg-fn'
+ `,
+		},
+		{
+			name: "Add new group to existing metrics annotation 2",
+			input: `
+apiVersion: compute.cnrm.cloud.google.com/v1beta1
+kind: ComputeSubnetwork
+metadata:
+  name: network-name-subnetwork
+  annotations:
+    cnrm.cloud.google.com/blueprint: 'kpt-pkg-fn'
+ `,
+			group: "live",
+			expected: `apiVersion: compute.cnrm.cloud.google.com/v1beta1
+kind: ComputeSubnetwork
+metadata:
+  name: network-name-subnetwork
+  annotations:
+    cnrm.cloud.google.com/blueprint: 'kpt-pkg-fn-live'
+ `,
+		},
+		{
+			name: "no-op if group is already present",
+			input: `
+apiVersion: compute.cnrm.cloud.google.com/v1beta1
+kind: ComputeSubnetwork
+metadata:
+  name: network-name-subnetwork
+  annotations:
+    cnrm.cloud.google.com/blueprint: 'kpt-pkg-fn-live'
+ `,
+			group: "fn",
+			expected: `apiVersion: compute.cnrm.cloud.google.com/v1beta1
+kind: ComputeSubnetwork
+metadata:
+  name: network-name-subnetwork
+  annotations:
+    cnrm.cloud.google.com/blueprint: 'kpt-pkg-fn-live'
+ `,
+		},
+		{
+			name: "add kpt prefix to existing annotation",
+			input: `
+apiVersion: compute.cnrm.cloud.google.com/v1beta1
+kind: ComputeSubnetwork
+metadata:
+  name: network-name-subnetwork
+  annotations:
+    cnrm.cloud.google.com/blueprint: cnrm/landing-zone:networking/v0.4.0
+ `,
+			group: "fn",
+			expected: `apiVersion: compute.cnrm.cloud.google.com/v1beta1
+kind: ComputeSubnetwork
+metadata:
+  name: network-name-subnetwork
+  annotations:
+    cnrm.cloud.google.com/blueprint: cnrm/landing-zone:networking/v0.4.0,kpt-fn
+ `,
+		},
+		{
+			name: "add group for existing annotation and existing kpt suffix",
+			input: `
+apiVersion: compute.cnrm.cloud.google.com/v1beta1
+kind: ComputeSubnetwork
+metadata:
+  name: network-name-subnetwork
+  annotations:
+    cnrm.cloud.google.com/blueprint: cnrm/landing-zone:networking/v0.4.0,kpt-fn
+ `,
+			group: "pkg",
+			expected: `apiVersion: compute.cnrm.cloud.google.com/v1beta1
+kind: ComputeSubnetwork
+metadata:
+  name: network-name-subnetwork
+  annotations:
+    cnrm.cloud.google.com/blueprint: cnrm/landing-zone:networking/v0.4.0,kpt-pkg-fn
+ `,
+		},
+		{
+			name: "add group for existing annotation and existing kpt substring",
+			input: `
+apiVersion: compute.cnrm.cloud.google.com/v1beta1
+kind: ComputeSubnetwork
+metadata:
+  name: network-name-subnetwork
+  annotations:
+    cnrm.cloud.google.com/blueprint: cnrm/landing-zone:networking/v0.4.0,kpt-fn,blueprints_controller
+ `,
+			group: "pkg",
+			expected: `apiVersion: compute.cnrm.cloud.google.com/v1beta1
+kind: ComputeSubnetwork
+metadata:
+  name: network-name-subnetwork
+  annotations:
+    cnrm.cloud.google.com/blueprint: cnrm/landing-zone:networking/v0.4.0,kpt-pkg-fn,blueprints_controller
+ `,
+		},
+	}
+	for i := range tests {
+		test := tests[i]
+		t.Run(test.name, func(t *testing.T) {
+			baseDir, err := ioutil.TempDir("", "")
+			if !assert.NoError(t, err) {
+				t.FailNow()
+			}
+			defer os.RemoveAll(baseDir)
+
+			r, err := ioutil.TempFile(baseDir, "k8s-cli-*.yaml")
+			if !assert.NoError(t, err) {
+				t.FailNow()
+			}
+			err = ioutil.WriteFile(r.Name(), []byte(test.input), 0600)
+			if !assert.NoError(t, err) {
+				t.FailNow()
+			}
+
+			if test.disable {
+				err = os.Setenv("KPT_DISABLE_ATTRIBUTION", "true")
+				if !assert.NoError(t, err) {
+					t.FailNow()
+				}
+				defer os.Setenv("KPT_DISABLE_ATTRIBUTION", "")
+			}
+
+			a := Attributor{PackagePaths: []string{baseDir}, CmdGroup: test.group}
+			a.Process()
+			actualResources, err := ioutil.ReadFile(r.Name())
+			if !assert.NoError(t, err) {
+				t.FailNow()
+			}
+			if !assert.Equal(t,
+				strings.TrimSpace(test.expected),
+				strings.TrimSpace(string(actualResources))) {
+				t.FailNow()
+			}
+		})
+	}
+}
diff --git a/internal/util/cmdutil/cmdutil_test.go b/internal/util/cmdutil/cmdutil_test.go
index fda429de..8fe1a91d 100644
--- a/internal/util/cmdutil/cmdutil_test.go
+++ b/internal/util/cmdutil/cmdutil_test.go
@@ -40,7 +40,7 @@ func TestWriteFnOutput(t *testing.T) {
 			name:   "wrapped output to stdout",
 			dest:   "stdout",
 			writer: bytes.Buffer{},
-			content: `apiVersion: config.kubernetes.io/v1alpha1
+			content: `apiVersion: config.kubernetes.io/v1
 kind: ResourceList
 items:
   - apiVersion: apps/v1
@@ -58,7 +58,7 @@ items:
         internal.config.kubernetes.io/index: '0'
         internal.config.kubernetes.io/path: 'svc.yaml'
 `,
-			expectedStdout: `apiVersion: config.kubernetes.io/v1alpha1
+			expectedStdout: `apiVersion: config.kubernetes.io/v1
 kind: ResourceList
 items:
   - apiVersion: apps/v1
@@ -81,7 +81,7 @@ items:
 			name:   "unwrapped output to stdout",
 			dest:   "unwrap",
 			writer: bytes.Buffer{},
-			content: `apiVersion: config.kubernetes.io/v1alpha1
+			content: `apiVersion: config.kubernetes.io/v1
 kind: ResourceList
 items:
   - apiVersion: apps/v1
@@ -113,7 +113,7 @@ metadata:
 		{
 			name: "output to another directory",
 			dest: "foo/bar",
-			content: `apiVersion: config.kubernetes.io/v1alpha1
+			content: `apiVersion: config.kubernetes.io/v1
 kind: ResourceList
 items:
   - apiVersion: apps/v1
@@ -152,7 +152,7 @@ metadata:
 			name:      "wrapped output to stdout by default if input is from stdin",
 			fromStdin: true,
 			writer:    bytes.Buffer{},
-			content: `apiVersion: config.kubernetes.io/v1alpha1
+			content: `apiVersion: config.kubernetes.io/v1
 kind: ResourceList
 items:
   - apiVersion: apps/v1
@@ -170,7 +170,7 @@ items:
         internal.config.kubernetes.io/index: '0'
         internal.config.kubernetes.io/path: 'svc.yaml'
 `,
-			expectedStdout: `apiVersion: config.kubernetes.io/v1alpha1
+			expectedStdout: `apiVersion: config.kubernetes.io/v1
 kind: ResourceList
 items:
   - apiVersion: apps/v1
@@ -194,7 +194,7 @@ items:
 			fromStdin: true,
 			dest:      "unwrap",
 			writer:    bytes.Buffer{},
-			content: `apiVersion: config.kubernetes.io/v1alpha1
+			content: `apiVersion: config.kubernetes.io/v1
 kind: ResourceList
 items:
   - apiVersion: apps/v1
@@ -227,7 +227,7 @@ metadata:
 			name:      "output to directory for input from stdin",
 			fromStdin: true,
 			dest:      "foo/bar",
-			content: `apiVersion: config.kubernetes.io/v1alpha1
+			content: `apiVersion: config.kubernetes.io/v1
 kind: ResourceList
 items:
   - apiVersion: apps/v1
diff --git a/pkg/test/live/config.go b/pkg/test/live/config.go
index 174f15a1..d55cf750 100644
--- a/pkg/test/live/config.go
+++ b/pkg/test/live/config.go
@@ -26,33 +26,30 @@ type TestCaseConfig struct {
 	// ExitCode is the expected exit code from the kpt commands. Default: 0
 	ExitCode int `yaml:"exitCode,omitempty"`
 
-	Output map[string]Output `yaml:"output,omitempty"`
+	// StdErr is the expected standard error output. Default: ""
+	StdErr string `yaml:"stdErr,omitempty"`
+
+	// StdOut is the expected standard output from running the command.
+	// Default: ""
+	StdOut string `yaml:"stdOut,omitempty"`
 
 	// Inventory is the expected list of resource present in the inventory.
 	Inventory []InventoryEntry `yaml:"inventory,omitempty"`
 
-	// RequiresCleanCluster tells the test framework that a new cluster must
-	// be created for running this test.
-	RequiresCleanCluster bool `yaml:"requiresCleanCluster,omitempty"`
+	// NoResourceGroup defines whether the RG CRD should be present in the cluster
+	// when the test starts.
+	NoResourceGroup bool `yaml:"noResourceGroup,omitempty"`
 
-	// PreinstallResourceGroup causes the test framework to verify that the
-	// ResourceGroup CRD is available in the cluster before running the test.
-	PreinstallResourceGroup bool `yaml:"preinstallResourceGroup,omitempty"`
+	// Parallel defines whether the test can be run in parallel with other
+	// tests. The primary requirement here is that the test doesn't create,
+	// update, or delete any cluster-scoped resources.
+	Parallel bool `yaml:"parallel,omitempty"`
 
 	// KptArgs is a list of args that will be provided to the kpt command
 	// when running the test.
 	KptArgs []string `yaml:"kptArgs,omitempty"`
 }
 
-type Output struct {
-	// StdErr is the expected standard error output. Default: ""
-	StdErr string `yaml:"stdErr,omitempty"`
-
-	// StdOut is the expected standard output from running the command.
-	// Default: ""
-	StdOut string `yaml:"stdOut,omitempty"`
-}
-
 // InventoryEntry defines an entry in an inventory list.
 type InventoryEntry struct {
 	Group     string `yaml:"group,omitempty"`
diff --git a/pkg/test/live/kind.go b/pkg/test/live/kind.go
new file mode 100644
index 00000000..10780562
--- /dev/null
+++ b/pkg/test/live/kind.go
@@ -0,0 +1,111 @@
+// 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 live
+
+import (
+	"bufio"
+	"bytes"
+	"fmt"
+	"os"
+	"os/exec"
+	"strings"
+	"testing"
+)
+
+const (
+	KindClusterName   = "live-e2e-test"
+	K8sVersionEnvName = "K8S_VERSION"
+)
+
+func InstallResourceGroup(t *testing.T) {
+	cmd := exec.Command("kpt", "live", "install-resource-group")
+	if err := cmd.Run(); err != nil {
+		t.Fatalf("error installing ResourceGroup CRD: %v", err)
+	}
+}
+
+func RemoveResourceGroup(t *testing.T) {
+	cmd := exec.Command("kubectl", "delete", "crd", "resourcegroups.kpt.dev")
+	if err := cmd.Run(); err != nil {
+		t.Fatalf("error removing ResourceGroup CRD: %v", err)
+	}
+	if CheckIfResourceGroupInstalled(t) {
+		t.Fatalf("couldn't remove ResourceGroup CRD")
+	}
+}
+
+func CheckIfResourceGroupInstalled(t *testing.T) bool {
+	cmd := exec.Command("kubectl", "get", "resourcegroups.kpt.dev")
+	output, err := cmd.CombinedOutput()
+	if strings.Contains(string(output), "the server doesn't have a resource type") {
+		return false
+	}
+	if err != nil {
+		t.Fatalf("error checking for ResourceGroup CRD: %v", err)
+	}
+	return true
+}
+
+func CheckKindClusterAvailable(t *testing.T) bool {
+	cmd := exec.Command("kind", "get", "clusters")
+	output, err := cmd.CombinedOutput()
+	if err != nil {
+		t.Fatalf("failed to check for kind cluster: %v", err)
+	}
+
+	sc := bufio.NewScanner(bytes.NewReader(output))
+	for sc.Scan() {
+		if strings.TrimSpace(sc.Text()) == KindClusterName {
+			return true
+		}
+	}
+	if err := sc.Err(); err != nil {
+		t.Fatalf("error parsing output from 'kind get cluster': %v", err)
+	}
+	return false
+}
+
+func CreateKindCluster(t *testing.T) {
+	args := []string{"create", "cluster", fmt.Sprintf("--name=%s", KindClusterName)}
+	if k8sVersion := os.Getenv(K8sVersionEnvName); k8sVersion != "" {
+		t.Logf("Using version %s", k8sVersion)
+		args = append(args, fmt.Sprintf("--image=kindest/node:v%s", k8sVersion))
+	}
+	cmd := exec.Command("kind", args...)
+	if err := cmd.Run(); err != nil {
+		t.Fatalf("failed to create new kind cluster: %v", err)
+	}
+}
+
+func RemoveKindCluster(t *testing.T) {
+	cmd := exec.Command("kind", "delete", "cluster", fmt.Sprintf("--name=%s", KindClusterName))
+	if err := cmd.Run(); err != nil {
+		t.Fatalf("failed to remove existing cluster: %v", err)
+	}
+}
+
+func CreateNamespace(t *testing.T, namespace string) {
+	cmd := exec.Command("kubectl", "create", "ns", namespace)
+	if err := cmd.Run(); err != nil {
+		t.Fatalf("error creating namespace %s: %v", namespace, err)
+	}
+}
+
+func RemoveNamespace(t *testing.T, namespace string) {
+	cmd := exec.Command("kubectl", "delete", "ns", namespace, "--wait=false")
+	if err := cmd.Run(); err != nil {
+		t.Logf("error deleting namespace %s: %v", namespace, err)
+	}
+}
diff --git a/pkg/test/live/runner.go b/pkg/test/live/runner.go
index d4720f0b..304644ab 100644
--- a/pkg/test/live/runner.go
+++ b/pkg/test/live/runner.go
@@ -18,7 +18,6 @@ import (
 	"bufio"
 	"bytes"
 	"errors"
-	"fmt"
 	"os"
 	"os/exec"
 	"path/filepath"
@@ -28,14 +27,10 @@ import (
 	"testing"
 
 	"github.com/stretchr/testify/assert"
+	"sigs.k8s.io/cli-utils/pkg/kstatus/status"
 	"sigs.k8s.io/kustomize/kyaml/yaml"
 )
 
-const (
-	KindClusterName   = "live-e2e-test"
-	K8sVersionEnvName = "K8S_VERSION"
-)
-
 // Runner uses the provided Config to run a test.
 type Runner struct {
 	// Config provides the configuration for how this test should be
@@ -44,41 +39,11 @@ type Runner struct {
 
 	// Path provides the path to the test files.
 	Path string
-
-	// Format provides the output format that should be used.
-	Format string
 }
 
 // Run executes the test.
 func (r *Runner) Run(t *testing.T) {
 	testName := filepath.Base(r.Path)
-	isAvailable := r.CheckKindClusterAvailable(t)
-	if r.Config.RequiresCleanCluster {
-		t.Log("Test requires clean cluster")
-		if isAvailable {
-			t.Log("Removing existing cluster")
-			r.RemoveKindCluster(t)
-		}
-		t.Log("Creating new cluster")
-		r.CreateKindCluster(t)
-	} else {
-		if !isAvailable {
-			t.Log("Creating new cluster")
-			r.CreateKindCluster(t)
-		} else if r.CheckForNamespace(t, testName) {
-			t.Log("Namespace already exist, creating new cluster")
-			r.RemoveKindCluster(t)
-			r.CreateKindCluster(t)
-		}
-	}
-
-	if r.Config.PreinstallResourceGroup {
-		r.InstallResourceGroup(t)
-	}
-
-	r.CreateNamespace(t, testName)
-	defer r.RemoveNamespace(t, testName)
-
 	r.RunPreApply(t)
 
 	stdout, stderr, err := r.RunApply(t)
@@ -108,9 +73,6 @@ func (r *Runner) RunPreApply(t *testing.T) {
 
 func (r *Runner) RunApply(t *testing.T) (string, string, error) {
 	args := append([]string{"live", "apply"}, r.Config.KptArgs...)
-	if r.Format != "" {
-		args = append(args, "--output", r.Format)
-	}
 	t.Logf("Running command: kpt %s", strings.Join(args, " "))
 	cmd := exec.Command("kpt", args...)
 	cmd.Dir = filepath.Join(r.Path, "resources")
@@ -124,98 +86,6 @@ func (r *Runner) RunApply(t *testing.T) (string, string, error) {
 	return outBuf.String(), errBuf.String(), err
 }
 
-func (r *Runner) InstallResourceGroup(t *testing.T) {
-	cmd := exec.Command("kpt", "live", "install-resource-group")
-	if err := cmd.Run(); err != nil {
-		t.Fatalf("error installing ResourceGroup CRD: %v", err)
-	}
-}
-
-func (r *Runner) CheckIfResourceGroupInstalled(t *testing.T) bool {
-	cmd := exec.Command("kubectl", "get", "resourcegroups.kpt.dev")
-	output, err := cmd.CombinedOutput()
-	if strings.Contains(string(output), "the server doesn't have a resource type") {
-		return false
-	}
-	if err != nil {
-		t.Fatalf("error checking for ResourceGroup CRD: %v", err)
-	}
-	return true
-}
-
-func (r *Runner) CheckKindClusterAvailable(t *testing.T) bool {
-	cmd := exec.Command("kind", "get", "clusters")
-	output, err := cmd.CombinedOutput()
-	if err != nil {
-		t.Fatalf("failed to check for kind cluster: %v", err)
-	}
-
-	sc := bufio.NewScanner(bytes.NewReader(output))
-	for sc.Scan() {
-		if strings.TrimSpace(sc.Text()) == KindClusterName {
-			return true
-		}
-	}
-	if err := sc.Err(); err != nil {
-		t.Fatalf("error parsing output from 'kind get cluster': %v", err)
-	}
-	return false
-}
-
-func (r *Runner) CreateKindCluster(t *testing.T) {
-	args := []string{"create", "cluster", fmt.Sprintf("--name=%s", KindClusterName)}
-	if k8sVersion := os.Getenv(K8sVersionEnvName); k8sVersion != "" {
-		t.Logf("Using version %s", k8sVersion)
-		args = append(args, fmt.Sprintf("--image=kindest/node:v%s", k8sVersion))
-	}
-	cmd := exec.Command("kind", args...)
-	if err := cmd.Run(); err != nil {
-		t.Fatalf("failed to create new kind cluster: %v", err)
-	}
-}
-
-func (r *Runner) RemoveKindCluster(t *testing.T) {
-	cmd := exec.Command("kind", "delete", "cluster", fmt.Sprintf("--name=%s", KindClusterName))
-	if err := cmd.Run(); err != nil {
-		t.Fatalf("failed to remove existing cluster: %v", err)
-	}
-}
-
-func (r *Runner) CheckForNamespace(t *testing.T, namespace string) bool {
-	cmd := exec.Command("kubectl", "get", "ns", namespace, "--no-headers", "--output=name")
-	output, err := cmd.CombinedOutput()
-	if strings.Contains(string(output), "NotFound") {
-		return false
-	}
-	if err != nil {
-		t.Fatalf("error listing namespaces with kubectl: %v", err)
-	}
-	sc := bufio.NewScanner(bytes.NewReader(output))
-	for sc.Scan() {
-		if strings.TrimSpace(sc.Text()) == fmt.Sprintf("namespace/%s", namespace) {
-			return true
-		}
-	}
-	if err := sc.Err(); err != nil {
-		t.Fatalf("error parsing output from 'kubectl get ns': %v", err)
-	}
-	return false
-}
-
-func (r *Runner) CreateNamespace(t *testing.T, namespace string) {
-	cmd := exec.Command("kubectl", "create", "ns", namespace)
-	if err := cmd.Run(); err != nil {
-		t.Fatalf("error creating namespace %s: %v", namespace, err)
-	}
-}
-
-func (r *Runner) RemoveNamespace(t *testing.T, namespace string) {
-	cmd := exec.Command("kubectl", "delete", "ns", namespace, "--wait=false")
-	if err := cmd.Run(); err != nil {
-		t.Logf("error deleting namespace %s: %v", namespace, err)
-	}
-}
-
 func (r *Runner) VerifyExitCode(t *testing.T, err error) {
 	exitCode := 0
 	var exitErr *exec.ExitError
@@ -228,11 +98,15 @@ func (r *Runner) VerifyExitCode(t *testing.T, err error) {
 }
 
 func (r *Runner) VerifyStdout(t *testing.T, stdout string) {
-	assert.Equal(t, strings.TrimSpace(r.Config.Output[r.Format].StdOut), strings.TrimSpace(substituteTimestamps(stdout)))
+	assert.Equal(t, strings.TrimSpace(r.Config.StdOut), prepOutput(t, stdout))
 }
 
 func (r *Runner) VerifyStderr(t *testing.T, stderr string) {
-	assert.Equal(t, strings.TrimSpace(r.Config.Output[r.Format].StdErr), strings.TrimSpace(substituteTimestamps(stderr)))
+	assert.Equal(t, strings.TrimSpace(r.Config.StdErr), prepOutput(t, stderr))
+}
+
+func prepOutput(t *testing.T, s string) string {
+	return strings.TrimSpace(substituteTimestamps(removeStatusEvents(t, s)))
 }
 
 func (r *Runner) VerifyInventory(t *testing.T, name, namespace string) {
@@ -305,3 +179,32 @@ var timestampRegexp = regexp.MustCompile(`\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z`)
 func substituteTimestamps(text string) string {
 	return timestampRegexp.ReplaceAllString(text, "<TIMESTAMP>")
 }
+
+var statuses = []status.Status{
+	status.InProgressStatus,
+	status.CurrentStatus,
+	status.FailedStatus,
+	status.TerminatingStatus,
+	status.UnknownStatus,
+	status.NotFoundStatus,
+}
+
+func removeStatusEvents(t *testing.T, text string) string {
+	scanner := bufio.NewScanner(strings.NewReader(text))
+	var lines []string
+
+scan:
+	for scanner.Scan() {
+		line := scanner.Text()
+		for _, s := range statuses {
+			if strings.Contains(line, s.String()) {
+				continue scan
+			}
+		}
+		lines = append(lines, line)
+	}
+	if err := scanner.Err(); err != nil {
+		t.Fatalf("error scanning output: %v", err)
+	}
+	return strings.Join(lines, "\n")
+}
diff --git a/thirdparty/cmdconfig/commands/cmdsink/cmdsink_test.go b/thirdparty/cmdconfig/commands/cmdsink/cmdsink_test.go
index c2916c0e..f9667184 100644
--- a/thirdparty/cmdconfig/commands/cmdsink/cmdsink_test.go
+++ b/thirdparty/cmdconfig/commands/cmdsink/cmdsink_test.go
@@ -26,7 +26,7 @@ func TestSinkCommand(t *testing.T) {
 	os.RemoveAll(d)
 
 	r := GetSinkRunner(fake.CtxWithDefaultPrinter(), "")
-	r.Command.SetIn(bytes.NewBufferString(`apiVersion: config.kubernetes.io/v1alpha1
+	r.Command.SetIn(bytes.NewBufferString(`apiVersion: config.kubernetes.io/v1
 kind: ResourceList
 items:
 - kind: Deployment
@@ -148,7 +148,7 @@ func TestSinkCommand_Error(t *testing.T) {
 	}
 
 	r := GetSinkRunner(fake.CtxWithDefaultPrinter(), "")
-	r.Command.SetIn(bytes.NewBufferString(`apiVersion: config.kubernetes.io/v1alpha1
+	r.Command.SetIn(bytes.NewBufferString(`apiVersion: config.kubernetes.io/v1
 kind: ResourceList
 items:
 - kind: Deployment
@@ -186,7 +186,7 @@ func TestSinkCommandJSON(t *testing.T) {
 	os.RemoveAll(d)
 
 	r := GetSinkRunner(fake.CtxWithDefaultPrinter(), "")
-	r.Command.SetIn(bytes.NewBufferString(`apiVersion: config.kubernetes.io/v1alpha1
+	r.Command.SetIn(bytes.NewBufferString(`apiVersion: config.kubernetes.io/v1
 kind: ResourceList
 items:
 - {"kind": "Deployment", "metadata": {"labels": {"app": "nginx2"}, "name": "foo",
diff --git a/thirdparty/cmdconfig/commands/cmdsource/cmdsource_test.go b/thirdparty/cmdconfig/commands/cmdsource/cmdsource_test.go
index 9607885b..a517fe64 100644
--- a/thirdparty/cmdconfig/commands/cmdsource/cmdsource_test.go
+++ b/thirdparty/cmdconfig/commands/cmdsource/cmdsource_test.go
@@ -81,7 +81,7 @@ spec:
 		return
 	}
 
-	if !assert.Equal(t, `apiVersion: config.kubernetes.io/v1alpha1
+	if !assert.Equal(t, `apiVersion: config.kubernetes.io/v1
 kind: ResourceList
 items:
 - kind: Deployment
@@ -353,7 +353,7 @@ spec:
 		return
 	}
 
-	if !assert.Equal(t, `apiVersion: config.kubernetes.io/v1alpha1
+	if !assert.Equal(t, `apiVersion: config.kubernetes.io/v1
 kind: ResourceList
 items:
 - kind: Deployment
@@ -476,7 +476,7 @@ func TestSourceCommandJSON(t *testing.T) {
 	}
 
 	//nolint:lll
-	expected := `apiVersion: config.kubernetes.io/v1alpha1
+	expected := `apiVersion: config.kubernetes.io/v1
 kind: ResourceList
 items:
 - {"kind": "Deployment", "metadata": {"labels": {"app": "nginx2"}, "name": "foo", "annotations": {"app": "nginx2", config.kubernetes.io/index: '0', config.kubernetes.io/path: 'f1.json', internal.config.kubernetes.io/index: '0', internal.config.kubernetes.io/path: 'f1.json', internal.config.kubernetes.io/seqindent: 'compact'}}, "spec": {"replicas": 1}}
@@ -562,7 +562,7 @@ spec:
 		return
 	}
 
-	if !assert.Equal(t, `apiVersion: config.kubernetes.io/v1alpha1
+	if !assert.Equal(t, `apiVersion: config.kubernetes.io/v1
 kind: ResourceList
 items:
 - kind: Deployment