Repository: GoogleContainerTools/kpt
Pull request #3167 (2022-05-13 20:45:08)

Update deployment section of porch guide
Updates the instructions for deployment in the porch guide:
* Add instructions for how to configure multirepo mode for Config Sync.
* Remove the namespace flag for the `get` and `delete` commands.



Patch:
diff --git a/Makefile b/Makefile
index 70eb9e51..f3ef0381 100644
--- a/Makefile
+++ b/Makefile
@@ -86,10 +86,14 @@ test-fn-render: build
 test-fn-eval: build
 	PATH=$(GOBIN):$(PATH) go test -v --tags=docker --run=TestFnEval/testdata/fn-eval/$(T)  ./e2e/
 
-# target to run e2e tests for "kpt fn eval" command
+# target to run e2e tests for "kpt live apply" command
 test-live-apply: build
 	PATH=$(GOBIN):$(PATH) go test -v -timeout=20m --tags=kind -p 2 --run=TestLiveApply/testdata/live-apply/$(T)  ./e2e/
 
+# target to run e2e tests for "kpt live plan" command
+test-live-plan: build
+	PATH=$(GOBIN):$(PATH) go test -v -timeout=20m --tags=kind -p 2 --run=TestLivePlan/testdata/live-plan/$(T)  ./e2e/
+
 test-porch: build
 	PATH=$(GOBIN):$(PATH) go test -v --count=1 --tags=porch ./e2e/
 
diff --git a/README.md b/README.md
index d1a721f4..ed268c08 100644
--- a/README.md
+++ b/README.md
@@ -36,7 +36,7 @@ The kpt toolchain includes the following components:
   the YAML KRM input/output format, but we provide SDKs to simplify the function authoring process, in 
   [Go](https://kpt.dev/book/05-developing-functions/02-developing-in-Go), 
   [Typescript](https://kpt.dev/book/05-developing-functions/03-developing-in-Typescript), and 
-  [Starlark](https://catalog.kpt.dev/starlark/v0.2/), a Python-like embedded language.
+  [Starlark](https://kpt.dev/book/05-developing-functions/04-developing-in-Starlark), a Python-like embedded language.
 
 - **Function catalog**: A [catalog](https://catalog.kpt.dev/) of off-the-shelf, tested functions. kpt makes configuration
   easy to create and transform, via reusable functions. Because they are expected to be used for in-place transformation,
@@ -49,14 +49,14 @@ The kpt toolchain includes the following components:
   similar to operations directly on the live state through the Kubernetes API.
 
 - **Config Sync**: While the package orchestrator
-  can be used with any GitOps tool, Config Sync provides a reference GitOps implementation to complete the WYSIWYG management
+  can be used with any GitOps tool, [Config Sync](https://github.com/GoogleContainerTools/kpt-config-sync) provides a reference GitOps implementation to complete the WYSIWYG management
   experience and enable end-to-end development of new features, such as 
   [OCI-based packages](https://github.com/GoogleContainerTools/kpt/issues/2300). Config Sync is also helping to drive improvements
   in upstream Kubernetes. For instance, Config Sync is built on top of [git-sync](https://github.com/kubernetes/git-sync) and
   leverages [Kustomize](https://kustomize.io) to automatically render manifests on the fly when needed. It uses the same apply
   logic as the kpt CLI.
 
-- **Backstage UI plugin**: We've created a proof-of-concept UI to demonstrate the WYSIWYG experience that's possible on top of the
+- **Backstage UI plugin**: We've created a proof-of-concept UI in the form of a [Backstage UI plugin](https://github.com/GoogleContainerTools/kpt-backstage-plugins) to demonstrate the WYSIWYG experience that's possible on top of the
   package orchestrator. More scenarios can be supported by implementing form-based editors for additional Kubernetes resource types.
 
 ## Roadmap
diff --git a/commands/alphacmd.go b/commands/alphacmd.go
index ba4e9b4c..70142333 100644
--- a/commands/alphacmd.go
+++ b/commands/alphacmd.go
@@ -44,6 +44,7 @@ func GetAlphaCommand(ctx context.Context, name, version string) *cobra.Command {
 		NewRepoCommand(ctx, version),
 		NewRpkgCommand(ctx, version),
 		NewSyncCommand(ctx, version),
+		GetAlphaLiveCommand(ctx, "", version),
 	)
 
 	return alpha
diff --git a/commands/alphalivecmd.go b/commands/alphalivecmd.go
new file mode 100644
index 00000000..b5794f2f
--- /dev/null
+++ b/commands/alphalivecmd.go
@@ -0,0 +1,46 @@
+// Copyright 2020 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 commands
+
+import (
+	"context"
+	"os"
+
+	"github.com/GoogleContainerTools/kpt/internal/cmdplan"
+	"github.com/GoogleContainerTools/kpt/internal/docs/generated/livedocs"
+	"github.com/spf13/cobra"
+	"k8s.io/cli-runtime/pkg/genericclioptions"
+)
+
+func GetAlphaLiveCommand(ctx context.Context, _, version string) *cobra.Command {
+	liveCmd := &cobra.Command{
+		Use:   "live",
+		Short: "[Alpha] " + livedocs.LiveShort,
+		Long:  "[Alpha] " + livedocs.LiveShort + "\n" + livedocs.LiveLong,
+	}
+
+	ioStreams := genericclioptions.IOStreams{
+		In:     os.Stdin,
+		Out:    os.Stdout,
+		ErrOut: os.Stderr,
+	}
+
+	f := newFactory(liveCmd, version)
+
+	planCmd := cmdplan.NewCommand(ctx, f, ioStreams)
+	liveCmd.AddCommand(planCmd)
+
+	return liveCmd
+}
diff --git a/commands/factory.go b/commands/factory.go
new file mode 100644
index 00000000..7d548d96
--- /dev/null
+++ b/commands/factory.go
@@ -0,0 +1,37 @@
+// Copyright 2020 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 commands
+
+import (
+	"flag"
+	"fmt"
+
+	"github.com/GoogleContainerTools/kpt/internal/util/cfgflags"
+	"github.com/spf13/cobra"
+	"k8s.io/cli-runtime/pkg/genericclioptions"
+	cluster "k8s.io/kubectl/pkg/cmd/util"
+)
+
+func newFactory(cmd *cobra.Command, version string) cluster.Factory {
+	flags := cmd.PersistentFlags()
+	kubeConfigFlags := genericclioptions.NewConfigFlags(true).WithDeprecatedPasswordFlag()
+	kubeConfigFlags.AddFlags(flags)
+	userAgentKubeConfigFlags := &cfgflags.UserAgentKubeConfigFlags{
+		Delegate:  kubeConfigFlags,
+		UserAgent: fmt.Sprintf("kpt/%s", version),
+	}
+	cmd.PersistentFlags().AddGoFlagSet(flag.CommandLine)
+	return cluster.NewFactory(userAgentKubeConfigFlags)
+}
diff --git a/commands/livecmd.go b/commands/livecmd.go
index cadaace9..880effa7 100644
--- a/commands/livecmd.go
+++ b/commands/livecmd.go
@@ -16,8 +16,6 @@ package commands
 
 import (
 	"context"
-	"flag"
-	"fmt"
 	"os"
 
 	"github.com/GoogleContainerTools/kpt/internal/cmdapply"
@@ -26,12 +24,10 @@ import (
 	"github.com/GoogleContainerTools/kpt/internal/cmdliveinit"
 	"github.com/GoogleContainerTools/kpt/internal/cmdmigrate"
 	"github.com/GoogleContainerTools/kpt/internal/docs/generated/livedocs"
-	"github.com/GoogleContainerTools/kpt/internal/util/cfgflags"
 	"github.com/GoogleContainerTools/kpt/thirdparty/cli-utils/status"
 	"github.com/spf13/cobra"
 	"k8s.io/cli-runtime/pkg/genericclioptions"
 	"k8s.io/klog/v2"
-	cluster "k8s.io/kubectl/pkg/cmd/util"
 	"sigs.k8s.io/cli-utils/pkg/manifestreader"
 )
 
@@ -71,15 +67,3 @@ func GetLiveCommand(ctx context.Context, _, version string) *cobra.Command {
 
 	return liveCmd
 }
-
-func newFactory(cmd *cobra.Command, version string) cluster.Factory {
-	flags := cmd.PersistentFlags()
-	kubeConfigFlags := genericclioptions.NewConfigFlags(true).WithDeprecatedPasswordFlag()
-	kubeConfigFlags.AddFlags(flags)
-	userAgentKubeConfigFlags := &cfgflags.UserAgentKubeConfigFlags{
-		Delegate:  kubeConfigFlags,
-		UserAgent: fmt.Sprintf("kpt/%s", version),
-	}
-	cmd.PersistentFlags().AddGoFlagSet(flag.CommandLine)
-	return cluster.NewFactory(userAgentKubeConfigFlags)
-}
diff --git a/docs/sync/prototype-controller.md b/docs/sync/prototype-controller.md
new file mode 100644
index 00000000..f44fbbf1
--- /dev/null
+++ b/docs/sync/prototype-controller.md
@@ -0,0 +1,23 @@
+## RemoteRootSync controller
+
+Create some example repositories / packages:
+
+```sh
+# Create artifact-registry repos etc
+make apply-dev-config
+# Push a sample hello-world app
+make -C config/samples/apps/hello-server push-image
+# Create a package for the sample hello-world app
+./scripts/create-deployment-package.sh
+```
+
+To test out remoterootsync self-applying:
+
+```sh
+# Grant more RBAC permissions than are normally needed (equivalent to admin permissions)
+kubectl apply -f controllers/remoterootsync/config/samples/hack-self-apply-rbac.yaml
+
+# Apply the RemoteRootSyncSet
+cat controllers/remoterootsync/config/samples/hack-self-apply.yaml \
+  | sed -e s/example-google-project-id/${GCP_PROJECT_ID}/g | kubectl apply -f -
+```
diff --git a/go.mod b/go.mod
index 1949a278..bb63ab7f 100644
--- a/go.mod
+++ b/go.mod
@@ -86,7 +86,7 @@ require (
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/russross/blackfriday v1.5.2 // indirect
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
-	github.com/sergi/go-diff v1.1.0 // indirect
+	github.com/sergi/go-diff v1.2.0 // indirect
 	github.com/spf13/pflag v1.0.5 // indirect
 	github.com/spyzhov/ajson v0.4.2 // indirect
 	go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect
diff --git a/go.sum b/go.sum
index 7bb5633f..e1c2e83f 100644
--- a/go.sum
+++ b/go.sum
@@ -582,8 +582,9 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
 github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
 github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig=
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
-github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
 github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
+github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
+github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
diff --git a/internal/cmdplan/command.go b/internal/cmdplan/command.go
new file mode 100644
index 00000000..cb8db7cc
--- /dev/null
+++ b/internal/cmdplan/command.go
@@ -0,0 +1,288 @@
+// Copyright 2022 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 cmdplan
+
+import (
+	"bufio"
+	"context"
+	"fmt"
+	"os"
+	"strings"
+
+	"github.com/GoogleContainerTools/kpt/internal/util/argutil"
+	"github.com/GoogleContainerTools/kpt/pkg/api/plan/v1alpha1"
+	"github.com/GoogleContainerTools/kpt/pkg/live"
+	kptplanner "github.com/GoogleContainerTools/kpt/pkg/live/planner"
+	"github.com/spf13/cobra"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/cli-runtime/pkg/genericclioptions"
+	"k8s.io/kubectl/pkg/cmd/util"
+	"sigs.k8s.io/cli-utils/cmd/flagutils"
+	"sigs.k8s.io/cli-utils/pkg/common"
+	print "sigs.k8s.io/cli-utils/pkg/print/common"
+	"sigs.k8s.io/kustomize/kyaml/kio"
+	"sigs.k8s.io/kustomize/kyaml/yaml"
+)
+
+const (
+	TextOutput = "text"
+	KRMOutput  = "krm"
+
+	EntryPrefix   = "\t"
+	ContentPrefix = "\t\t"
+)
+
+func NewRunner(ctx context.Context, factory util.Factory, ioStreams genericclioptions.IOStreams) *Runner {
+	r := &Runner{
+		ctx:       ctx,
+		factory:   factory,
+		ioStreams: ioStreams,
+		serverSideOptions: common.ServerSideOptions{
+			ServerSideApply: true,
+		},
+	}
+	c := &cobra.Command{
+		Use:     "plan [PKG_PATH | -]",
+		PreRunE: r.PreRunE,
+		RunE:    r.RunE,
+	}
+	c.Flags().StringVar(&r.inventoryPolicyString, flagutils.InventoryPolicyFlag, flagutils.InventoryPolicyStrict,
+		"It determines the behavior when the resources don't belong to current inventory. Available options "+
+			fmt.Sprintf("%q and %q.", flagutils.InventoryPolicyStrict, flagutils.InventoryPolicyAdopt))
+	c.Flags().BoolVar(&r.serverSideOptions.ForceConflicts, "force-conflicts", false,
+		"If true, overwrite applied fields on server if field manager conflict.")
+	c.Flags().StringVar(&r.serverSideOptions.FieldManager, "field-manager", common.DefaultFieldManager,
+		"The client owner of the fields being applied on the server-side.")
+	c.Flags().StringVar(&r.output, "output", "text",
+		"The output format for the plan. Must be either 'text' or 'krm'. Default is 'text'")
+	r.Command = c
+
+	return r
+}
+
+func NewCommand(ctx context.Context, factory util.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
+	return NewRunner(ctx, factory, ioStreams).Command
+}
+
+type Runner struct {
+	ctx       context.Context
+	Command   *cobra.Command
+	factory   util.Factory
+	ioStreams genericclioptions.IOStreams
+
+	inventoryPolicyString string
+	serverSideOptions     common.ServerSideOptions
+	output                string
+}
+
+func (r *Runner) PreRunE(c *cobra.Command, args []string) error {
+	return r.validateOutputFormat()
+}
+
+func (r *Runner) validateOutputFormat() error {
+	if !(r.output == "text" || r.output == "krm") {
+		return fmt.Errorf("unknown output format %q. Must be either 'text' or 'krm'", r.output)
+	}
+	return nil
+}
+
+func (r *Runner) RunE(c *cobra.Command, args []string) error {
+	// default to the current working directory if the user didn't
+	// provide a target package.
+	if len(args) == 0 {
+		cwd, err := os.Getwd()
+		if err != nil {
+			return err
+		}
+		args = append(args, cwd)
+	}
+
+	// Handle symlinks.
+	path := args[0]
+	var err error
+	if args[0] != "-" {
+		path, err = argutil.ResolveSymlink(r.ctx, path)
+		if err != nil {
+			return err
+		}
+	}
+
+	// Load the resources from disk or stdin and extract the
+	// inventory information.
+	objs, inv, err := live.Load(r.factory, path, "", c.InOrStdin())
+	if err != nil {
+		return err
+	}
+
+	// Convert the inventory data input to the format required by
+	// the actuation code.
+	invInfo, err := live.ToInventoryInfo(inv)
+	if err != nil {
+		return err
+	}
+
+	// Create and execute the planner.
+	planner, err := kptplanner.NewClusterPlanner(r.factory)
+	if err != nil {
+		return err
+	}
+	plan, err := planner.BuildPlan(r.ctx, invInfo, objs, kptplanner.Options{
+		ServerSideOptions: r.serverSideOptions,
+	})
+	if err != nil {
+		return err
+	}
+
+	switch r.output {
+	case "text":
+		return printText(plan, objs, r.ioStreams)
+	case "krm":
+		return printKRM(plan, r.ioStreams)
+	}
+	return fmt.Errorf("unknown output format %s", r.output)
+}
+
+func printText(plan *v1alpha1.Plan, objs []*unstructured.Unstructured, ioStreams genericclioptions.IOStreams) error {
+	if !hasChanges(plan) {
+		fmt.Fprint(ioStreams.Out, "no changes found\n")
+		return nil
+	}
+
+	fmt.Fprintf(ioStreams.Out, "kpt will perform the following actions:\n")
+	for i := range plan.Spec.Actions {
+		action := plan.Spec.Actions[i]
+		switch action.Type {
+		case v1alpha1.Create:
+			printEntryWithColor("+", print.GREEN, action, ioStreams)
+			u, ok := findResource(objs, action.Group, action.Kind, action.Namespace, action.Name)
+			if !ok {
+				panic("can't find resource")
+			}
+			printKRMWithPrefix(u, ContentPrefix, ioStreams)
+		case v1alpha1.Unchanged:
+			// Do nothing.
+		case v1alpha1.Delete:
+			printEntryWithColor("-", print.RED, action, ioStreams)
+		case v1alpha1.Update:
+			printEntry(" ", action, ioStreams)
+			findAndPrintDiff(action.Before, action.After, ContentPrefix, ioStreams)
+		case v1alpha1.Skip:
+			// TODO: provide more information about why the resource was skipped.
+			printEntryWithColor("=", print.YELLOW, action, ioStreams)
+		case v1alpha1.Error:
+			printEntry("!", action, ioStreams)
+			printWithPrefix(action.Error, ContentPrefix, ioStreams)
+		}
+		fmt.Fprintf(ioStreams.Out, "\n")
+	}
+	return nil
+}
+
+func hasChanges(plan *v1alpha1.Plan) bool {
+	for _, a := range plan.Spec.Actions {
+		if a.Type != v1alpha1.Unchanged {
+			return true
+		}
+	}
+	return false
+}
+
+func printEntryWithColor(prefix string, color print.Color, action v1alpha1.Action, ioStreams genericclioptions.IOStreams) {
+	txt := print.SprintfWithColor(color, "%s%s %s/%s %s/%s\n", EntryPrefix, prefix, action.Group, action.Kind, action.Namespace, action.Name)
+	fmt.Fprint(ioStreams.Out, txt)
+}
+
+func printEntry(prefix string, action v1alpha1.Action, ioStreams genericclioptions.IOStreams) {
+	fmt.Fprintf(ioStreams.Out, "%s%s %s/%s %s/%s\n", EntryPrefix, prefix, action.Group, action.Kind, action.Namespace, action.Name)
+}
+
+func findAndPrintDiff(before, after *unstructured.Unstructured, prefix string, ioStreams genericclioptions.IOStreams) {
+	diff, err := diffObjects(before, after)
+	if err != nil {
+		panic(err)
+	}
+	for _, d := range diff {
+		if d.Path == ".metadata.generation" || d.Path == ".metadata.managedFields.0.time" {
+			continue
+		}
+
+		switch d.Type {
+		case "LeftAdd":
+			txt := print.SprintfWithColor(print.RED, "%s-%s: %s\n", prefix, d.Path, strings.TrimSpace(fmt.Sprintf("%v", d.Left)))
+			fmt.Fprint(ioStreams.Out, txt)
+		case "RightAdd":
+			txt := print.SprintfWithColor(print.GREEN, "%s+%s: %s\n", prefix, d.Path, strings.TrimSpace(fmt.Sprintf("%v", d.Right)))
+			fmt.Fprint(ioStreams.Out, txt)
+		case "Change":
+			txt1 := print.SprintfWithColor(print.RED, "%s-%s: %s\n", prefix, d.Path, strings.TrimSpace(fmt.Sprintf("%v", d.Left)))
+			fmt.Fprint(ioStreams.Out, txt1)
+			txt2 := print.SprintfWithColor(print.GREEN, "%s+%s: %s\n", prefix, d.Path, strings.TrimSpace(fmt.Sprintf("%v", d.Right)))
+			fmt.Fprint(ioStreams.Out, txt2)
+		}
+	}
+}
+
+func findResource(objs []*unstructured.Unstructured, group, kind, namespace, name string) (*unstructured.Unstructured, bool) {
+	for i := range objs {
+		o := objs[i]
+		gvk := o.GroupVersionKind()
+		if gvk.Group == group && gvk.Kind == kind && o.GetName() == name && o.GetNamespace() == namespace {
+			return o, true
+		}
+	}
+	return nil, false
+}
+
+func printKRMWithPrefix(u *unstructured.Unstructured, prefix string, ioStreams genericclioptions.IOStreams) {
+	b, err := yaml.Marshal(u.Object)
+	if err != nil {
+		panic(fmt.Errorf("unable to marshal resource: %v", err))
+	}
+	printWithPrefix(string(b), prefix, ioStreams)
+}
+
+func printWithPrefix(text, prefix string, ioStreams genericclioptions.IOStreams) {
+	scanner := bufio.NewScanner(strings.NewReader(text))
+	for scanner.Scan() {
+		fmt.Fprintf(ioStreams.Out, "%s%s\n", prefix, scanner.Text())
+	}
+	if err := scanner.Err(); err != nil {
+		panic(fmt.Errorf("error reading text: %v", err))
+	}
+}
+
+// printKRM outputs the plan inside a ResourceList so the output format
+// follows the KRM function wire format.
+func printKRM(plan *v1alpha1.Plan, ioStreams genericclioptions.IOStreams) error {
+	b, err := yaml.Marshal(plan)
+	if err != nil {
+		return fmt.Errorf("failed to marshall plan into yaml: %w", err)
+	}
+	rn, err := yaml.Parse(string(b))
+	if err != nil {
+		return fmt.Errorf("failed to parse plan: %w", err)
+	}
+	writer := &kio.ByteWriter{
+		Writer:                ioStreams.Out,
+		KeepReaderAnnotations: true,
+		WrappingAPIVersion:    kio.ResourceListAPIVersion,
+		WrappingKind:          kio.ResourceListKind,
+	}
+	err = writer.Write([]*yaml.RNode{rn})
+	if err != nil {
+		return fmt.Errorf("failed to write resources: %w", err)
+	}
+	return nil
+}
diff --git a/internal/cmdplan/diff.go b/internal/cmdplan/diff.go
new file mode 100644
index 00000000..f4ddc2a5
--- /dev/null
+++ b/internal/cmdplan/diff.go
@@ -0,0 +1,130 @@
+// Copyright 2022 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 cmdplan
+
+import (
+	"reflect"
+	"strconv"
+
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+)
+
+type Diff struct {
+	Type  string
+	Left  interface{}
+	Right interface{}
+	Path  string
+}
+
+func diffObjects(b, a *unstructured.Unstructured) ([]Diff, error) {
+	diffs, err := diffMaps("", b.Object, a.Object)
+	if err != nil {
+		return nil, err
+	}
+	return diffs, nil
+}
+
+func diffMaps(prefix string, l, r map[string]interface{}) ([]Diff, error) {
+	var diffs []Diff
+	for k, lv := range l {
+		childPrefix := prefix + "." + k
+
+		rv, ok := r[k]
+		if !ok {
+			diffs = append(diffs, Diff{Type: "LeftAdd", Path: childPrefix, Left: lv, Right: rv})
+			continue
+		}
+		childDiffs, err := diffValue(childPrefix, lv, rv)
+		if err != nil {
+			return nil, err
+		}
+		diffs = append(diffs, childDiffs...)
+	}
+
+	for k, rv := range r {
+		childPrefix := prefix + "." + k
+
+		lv, ok := l[k]
+		if !ok {
+			diffs = append(diffs, Diff{Type: "RightAdd", Path: childPrefix, Left: lv, Right: rv})
+			continue
+		}
+	}
+
+	return diffs, nil
+}
+
+func diffSlices(prefix string, l, r []interface{}) ([]Diff, error) {
+	var diffs []Diff
+	for i, lv := range l {
+		childPrefix := prefix + "." + strconv.Itoa(i)
+
+		if len(r) <= i {
+			diffs = append(diffs, Diff{Type: "LeftAdd", Path: childPrefix, Left: l[i], Right: nil})
+			continue
+		}
+		rv := r[i]
+		childDiffs, err := diffValue(childPrefix, lv, rv)
+		if err != nil {
+			return nil, err
+		}
+		diffs = append(diffs, childDiffs...)
+	}
+
+	for i, rv := range r {
+		childPrefix := prefix + "." + strconv.Itoa(i)
+
+		if len(l) <= i {
+			diffs = append(diffs, Diff{Type: "RightAdd", Path: childPrefix, Left: nil, Right: rv})
+			continue
+		}
+	}
+
+	return diffs, nil
+}
+
+func diffValue(path string, lv, rv interface{}) ([]Diff, error) {
+	switch lv := lv.(type) {
+	// case string:
+	// 	rvString, ok := rv.(string)
+	// 	if !ok || lv != rvString {
+	// 		diffs = append(diffs, Diff{Type: "Change", Path: childPrefix, Left: lv, Right: rv})
+	// 	}
+	// case int64:
+	// 	rvInt64, ok := rv.(int64)
+	// 	if !ok || lv != rvInt64 {
+	// 		diffs = append(diffs, Diff{Type: "Change", Path: childPrefix, Left: lv, Right: rv})
+	// 	}
+	case map[string]interface{}:
+		rvMap, ok := rv.(map[string]interface{})
+		if !ok {
+			return []Diff{{Type: "Change", Path: path, Left: lv, Right: rv}}, nil
+		}
+		return diffMaps(path, lv, rvMap)
+
+	case []interface{}:
+		rvSlice, ok := rv.([]interface{})
+		if !ok {
+			return []Diff{{Type: "Change", Path: path, Left: lv, Right: rv}}, nil
+		}
+		return diffSlices(path, lv, rvSlice)
+
+	default:
+		if !reflect.DeepEqual(lv, rv) {
+			return []Diff{{Type: "Change", Path: path, Left: lv, Right: rv}}, nil
+		}
+		return nil, nil
+	}
+}
diff --git a/pkg/api/plan/v1alpha1/types.go b/pkg/api/plan/v1alpha1/types.go
new file mode 100644
index 00000000..bd5e17a4
--- /dev/null
+++ b/pkg/api/plan/v1alpha1/types.go
@@ -0,0 +1,67 @@
+// Copyright 2022 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 v1alpha1
+
+import (
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"sigs.k8s.io/kustomize/kyaml/yaml"
+)
+
+var ResourceMeta = yaml.ResourceMeta{
+	TypeMeta: yaml.TypeMeta{
+		APIVersion: "config.google.com/v1alpha1",
+		Kind:       "Plan",
+	},
+	ObjectMeta: yaml.ObjectMeta{
+		NameMeta: yaml.NameMeta{
+			Name: "plan",
+		},
+		Annotations: map[string]string{
+			"config.kubernetes.io/local-config": "true",
+		},
+	},
+}
+
+type Plan struct {
+	yaml.ResourceMeta `yaml:",inline" json:",inline"`
+
+	Spec PlanSpec `json:"spec,omitempty" yaml:"spec,omitempty"`
+}
+
+type PlanSpec struct {
+	Actions []Action `json:"actions,omitempty"`
+}
+
+type ActionType string
+
+const (
+	Create    ActionType = "Create"
+	Unchanged ActionType = "Unchanged"
+	Delete    ActionType = "Delete"
+	Update    ActionType = "Update"
+	Skip      ActionType = "Skip"
+	Error     ActionType = "Error"
+)
+
+type Action struct {
+	Type      ActionType                 `json:"action,omitempty" yaml:"action,omitempty"`
+	Group     string                     `json:"apiVersion,omitempty" yaml:"apiVersion,omitempty"`
+	Kind      string                     `json:"kind,omitempty" yaml:"kind,omitempty"`
+	Name      string                     `json:"name,omitempty" yaml:"name,omitempty"`
+	Namespace string                     `json:"namespace,omitempty" yaml:"namespace,omitempty"`
+	Before    *unstructured.Unstructured `json:"before,omitempty" yaml:"before,omitempty"`
+	After     *unstructured.Unstructured `json:"after,omitempty" yaml:"after,omitempty"`
+	Error     string                     `json:"error,omitempty" yaml:"error,omitempty"`
+}
diff --git a/pkg/live/planner/cluster.go b/pkg/live/planner/cluster.go
new file mode 100644
index 00000000..4d79ccd8
--- /dev/null
+++ b/pkg/live/planner/cluster.go
@@ -0,0 +1,300 @@
+// Copyright 2022 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 planner
+
+import (
+	"context"
+	"fmt"
+	"reflect"
+
+	planv1alpha1 "github.com/GoogleContainerTools/kpt/pkg/api/plan/v1alpha1"
+	"github.com/GoogleContainerTools/kpt/pkg/live"
+	"github.com/GoogleContainerTools/kpt/pkg/status"
+	"github.com/GoogleContainerTools/kpt/thirdparty/cli-utils/apply"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/api/meta"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/client-go/dynamic"
+	"k8s.io/kubectl/pkg/cmd/util"
+	"sigs.k8s.io/cli-utils/pkg/apply/event"
+	"sigs.k8s.io/cli-utils/pkg/common"
+	"sigs.k8s.io/cli-utils/pkg/inventory"
+	"sigs.k8s.io/cli-utils/pkg/object"
+)
+
+func NewClusterPlanner(f util.Factory) (*ClusterPlanner, error) {
+	fetcher, err := NewResourceFetcher(f)
+	if err != nil {
+		return nil, err
+	}
+
+	invClient, err := inventory.NewClient(f, live.WrapInventoryObj, live.InvToUnstructuredFunc, inventory.StatusPolicyNone)
+	if err != nil {
+		return nil, err
+	}
+
+	statusPoller, err := status.NewStatusPoller(f)
+	if err != nil {
+		return nil, err
+	}
+
+	applier, err := apply.NewApplierBuilder().
+		WithFactory(f).
+		WithInventoryClient(invClient).
+		WithStatusPoller(statusPoller).
+		Build()
+	if err != nil {
+		return nil, err
+	}
+
+	return &ClusterPlanner{
+		applier:         applier,
+		resourceFetcher: fetcher,
+	}, nil
+}
+
+type Applier interface {
+	Run(ctx context.Context, invInfo inventory.Info, objects object.UnstructuredSet, options apply.ApplierOptions) <-chan event.Event
+}
+
+type ResourceFetcher interface {
+	FetchResource(ctx context.Context, id object.ObjMetadata) (*unstructured.Unstructured, bool, error)
+}
+
+type ClusterPlanner struct {
+	applier         Applier
+	resourceFetcher ResourceFetcher
+}
+
+type Options struct {
+	ServerSideOptions common.ServerSideOptions
+}
+
+func (r *ClusterPlanner) BuildPlan(ctx context.Context, inv inventory.Info, objects []*unstructured.Unstructured, o Options) (*planv1alpha1.Plan, error) {
+	actions, err := r.dryRunForPlan(ctx, inv, objects, o)
+	if err != nil {
+		return nil, err
+	}
+	return &planv1alpha1.Plan{
+		ResourceMeta: planv1alpha1.ResourceMeta,
+		Spec: planv1alpha1.PlanSpec{
+			Actions: actions,
+		},
+	}, nil
+}
+
+func (r *ClusterPlanner) dryRunForPlan(
+	ctx context.Context,
+	inv inventory.Info,
+	objects []*unstructured.Unstructured,
+	o Options,
+) ([]planv1alpha1.Action, error) {
+	eventCh := r.applier.Run(ctx, inv, objects, apply.ApplierOptions{
+		DryRunStrategy:    common.DryRunServer,
+		ServerSideOptions: o.ServerSideOptions,
+	})
+
+	var actions []planv1alpha1.Action
+	var err error
+	for e := range eventCh {
+		if e.Type == event.InitType {
+			// This event includes all resources that will be applied, pruned or deleted, so
+			// we make sure we fetch all the resources from the cluster.
+			// TODO: See if we can update the actuation library to provide the pre-actuation
+			// versions of the resources as part of the regular run. This solution is not great
+			// as fetching all resources will take time.
+			a, err := r.fetchResources(ctx, e)
+			if err != nil {
+				return nil, err
+			}
+			actions = a
+		}
+		if e.Type == event.ErrorType {
+			// Update the err variable here, but wait for the channel to close
+			// before we return from the function.
+			// Since ErrorEvents are considered fatal, there should only be sent
+			// and it will be followed by the channel being closed.
+			err = e.ErrorEvent.Err
+		}
+		// For the Apply, Prune and Delete event types, we just capture the result
+		// of the dry-run operation for the specific resource.
+		switch e.Type {
+		case event.ApplyType:
+			id := e.ApplyEvent.Identifier
+			index := indexForIdentifier(id, actions)
+			a := actions[index]
+			actions[index] = handleApplyEvent(e, a)
+		case event.PruneType:
+			id := e.PruneEvent.Identifier
+			index := indexForIdentifier(id, actions)
+			a := actions[index]
+			actions[index] = handlePruneEvent(e, a)
+		// Prune and Delete are essentially the same thing, but the actuation
+		// library return Prune events when resources are deleted by omission
+		// during apply, and Delete events from the destroyer. Supporting both
+		// here for completeness.
+		case event.DeleteType:
+			id := e.DeleteEvent.Identifier
+			index := indexForIdentifier(id, actions)
+			a := actions[index]
+			actions[index] = handleDeleteEvent(e, a)
+		}
+	}
+	return actions, err
+}
+
+func handleApplyEvent(e event.Event, a planv1alpha1.Action) planv1alpha1.Action {
+	if e.ApplyEvent.Error != nil {
+		a.Type = planv1alpha1.Error
+		a.Error = e.ApplyEvent.Error.Error()
+	} else {
+		switch e.ApplyEvent.Operation {
+		case event.Unchanged:
+			a.Type = planv1alpha1.Skip
+		case event.ServersideApplied:
+			a.After = e.ApplyEvent.Resource
+			if a.Before != nil {
+				// TODO: Unclear if we should diff the full resources here. It doesn't work
+				// well with client-side apply as the managedFields property shows up as
+				// changes. It also means there is a race with controllers that might change
+				// the status of resources.
+				if reflect.DeepEqual(a.Before, a.After) {
+					a.Type = planv1alpha1.Unchanged
+				} else {
+					a.Type = planv1alpha1.Update
+				}
+			} else {
+				a.Type = planv1alpha1.Create
+			}
+		}
+	}
+	return a
+}
+
+func handlePruneEvent(e event.Event, a planv1alpha1.Action) planv1alpha1.Action {
+	if e.PruneEvent.Error != nil {
+		a.Type = planv1alpha1.Error
+		a.Error = e.PruneEvent.Error.Error()
+	} else {
+		switch e.PruneEvent.Operation {
+		case event.Pruned:
+			a.Type = planv1alpha1.Delete
+		// Lifecycle directives can cause resources to remain in the
+		// live state even if they would normally be pruned.
+		// TODO: Handle reason for skipped resources that has recently
+		// been added to the actuation library.
+		case event.PruneSkipped:
+			a.Type = planv1alpha1.Skip
+		}
+	}
+	return a
+}
+
+func handleDeleteEvent(e event.Event, a planv1alpha1.Action) planv1alpha1.Action {
+	if e.DeleteEvent.Error != nil {
+		a.Type = planv1alpha1.Error
+		a.Error = e.DeleteEvent.Error.Error()
+	} else {
+		switch e.DeleteEvent.Operation {
+		case event.Deleted:
+			a.Type = planv1alpha1.Delete
+		case event.DeleteSkipped:
+			a.Type = planv1alpha1.Skip
+		}
+	}
+	return a
+}
+
+func (r *ClusterPlanner) fetchResources(ctx context.Context, e event.Event) ([]planv1alpha1.Action, error) {
+	var actions []planv1alpha1.Action
+	for _, ag := range e.InitEvent.ActionGroups {
+		// We only care about the Apply, Prune and Delete actions.
+		if !(ag.Action == event.ApplyAction || ag.Action == event.PruneAction || ag.Action == event.DeleteAction) {
+			continue
+		}
+		for _, id := range ag.Identifiers {
+			u, _, err := r.resourceFetcher.FetchResource(ctx, id)
+			// If the type doesn't exist in the cluster, then the resource itself doesn't exist.
+			if err != nil && !meta.IsNoMatchError(err) {
+				return nil, err
+			}
+			actions = append(actions, planv1alpha1.Action{
+				Group:     id.GroupKind.Group,
+				Kind:      id.GroupKind.Kind,
+				Name:      id.Name,
+				Namespace: id.Namespace,
+				Before:    u,
+			})
+		}
+	}
+	return actions, nil
+}
+
+type resourceFetcher struct {
+	dynamicClient dynamic.Interface
+	mapper        meta.RESTMapper
+}
+
+func NewResourceFetcher(f util.Factory) (ResourceFetcher, error) {
+	dc, err := f.DynamicClient()
+	if err != nil {
+		return nil, err
+	}
+
+	mapper, err := f.ToRESTMapper()
+	if err != nil {
+		return nil, err
+	}
+	return &resourceFetcher{
+		dynamicClient: dc,
+		mapper:        mapper,
+	}, nil
+}
+
+func (rf *resourceFetcher) FetchResource(ctx context.Context, id object.ObjMetadata) (*unstructured.Unstructured, bool, error) {
+	mapping, err := rf.mapper.RESTMapping(id.GroupKind)
+	if err != nil {
+		return nil, false, err
+	}
+	var r dynamic.ResourceInterface
+	if mapping.Scope == meta.RESTScopeRoot {
+		r = rf.dynamicClient.Resource(mapping.Resource)
+	} else {
+		r = rf.dynamicClient.Resource(mapping.Resource).Namespace(id.Namespace)
+	}
+	u, err := r.Get(ctx, id.Name, metav1.GetOptions{})
+	if err != nil && !apierrors.IsNotFound(err) {
+		return nil, false, err
+	}
+
+	if apierrors.IsNotFound(err) {
+		return nil, false, nil
+	}
+	return u, true, nil
+}
+
+func indexForIdentifier(id object.ObjMetadata, actions []planv1alpha1.Action) int {
+	for i := range actions {
+		a := actions[i]
+		if a.Group == id.GroupKind.Group &&
+			a.Kind == id.GroupKind.Kind &&
+			a.Name == id.Name &&
+			a.Namespace == id.Namespace {
+			return i
+		}
+	}
+	panic(fmt.Errorf("unknown identifier %s", id.String()))
+}
diff --git a/porch/docs/development.md b/porch/docs/development.md
index 033ae41b..3efe63a9 100644
--- a/porch/docs/development.md
+++ b/porch/docs/development.md
@@ -55,11 +55,11 @@ IMAGE_TAG=$(git rev-parse --short HEAD) make push-images
 
 ## Running Locally
 
-Follow [running-locally.md](./running-locally-md) to run Porch locally.
+Follow [running-locally.md](./running-locally.md) to run Porch locally.
 
 ## Debugging
 
-To debug Porch, run Porch locally [running-locally.md](./running-locally-md), exit porch server running in the shell,
+To debug Porch, run Porch locally [running-locally.md](./running-locally.md), exit porch server running in the shell,
 and launch Porch under the debugger. VSCode debug session is preconfigured in [launch.json](../.vscode/launch.json).
 
 Update the launch arguments to your needs.
diff --git a/porch/go.mod b/porch/go.mod
index c370edfb..fa228920 100644
--- a/porch/go.mod
+++ b/porch/go.mod
@@ -142,7 +142,7 @@ require (
 	github.com/qri-io/starlib v0.5.0 // indirect
 	github.com/russross/blackfriday v1.5.2 // indirect
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
-	github.com/sergi/go-diff v1.1.0 // indirect
+	github.com/sergi/go-diff v1.2.0 // indirect
 	github.com/sirupsen/logrus v1.8.1 // indirect
 	github.com/stretchr/testify v1.7.1 // indirect
 	github.com/vbatts/tar-split v0.11.2 // indirect
diff --git a/porch/go.sum b/porch/go.sum
index dd16a645..623f593c 100644
--- a/porch/go.sum
+++ b/porch/go.sum
@@ -934,8 +934,9 @@ github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43
 github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
 github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
-github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
 github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
+github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
+github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
 github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
 github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
diff --git a/scripts/generate_site_sidebar/sidebar_template.md.tmpl b/scripts/generate_site_sidebar/sidebar_template.md.tmpl
index ea92ab77..6bc59b6b 100644
--- a/scripts/generate_site_sidebar/sidebar_template.md.tmpl
+++ b/scripts/generate_site_sidebar/sidebar_template.md.tmpl
@@ -57,11 +57,12 @@
 - [GitOps](gitops/)
   - [Config Sync](gitops/configsync/)
 - [Guides](guides/)
-  - [The Rationale behind kpt](guides/rationale.md)
-  - [Namespace provisioning CLI](guides/namespace-provisioning-cli.md)
-  - [Namespace provisioning UI](guides/namespace-provisioning-ui.md)
+  - [The Rationale Behind kpt](guides/rationale.md)
   - [Porch Installation Guide](guides/porch-installation.md)
+  - [Porch UI Installation Guide](guides/porch-ui-installation.md)
   - [Porch User Guide](guides/porch-user-guide.md)
+  - [Namespace Provisioning CLI](guides/namespace-provisioning-cli.md)
+  - [Namespace Provisioning UI](guides/namespace-provisioning-ui.md)
   - [Variant Constructor Pattern](guides/variant-constructor-pattern.md)
   - [Value Propagation Pattern](guides/value-propagation.md)
 - [FAQ](faq/)
diff --git a/site/book/02-concepts/00.md b/site/book/02-concepts/00.md
index 2f6e9c13..6f99c265 100644
--- a/site/book/02-concepts/00.md
+++ b/site/book/02-concepts/00.md
@@ -20,7 +20,7 @@ The kpt toolchain includes the following components:
   the YAML KRM input/output format, but we provide SDKs to simplify the function authoring process, in 
   [Go](https://kpt.dev/book/05-developing-functions/02-developing-in-Go), 
   [Typescript](https://kpt.dev/book/05-developing-functions/03-developing-in-Typescript), and 
-  [Starlark](https://catalog.kpt.dev/starlark/v0.2/), a Python-like embedded language.
+  [Starlark](https://kpt.dev/book/05-developing-functions/04-developing-in-Starlark), a Python-like embedded language.
 
 - [**Function catalog**](https://catalog.kpt.dev/): A catalog of off-the-shelf, tested functions. kpt makes configuration
   easy to create and transform, via reusable functions. Because they are expected to be used for in-place transformation,
diff --git a/site/book/05-developing-functions/02-developing-in-Go.md b/site/book/05-developing-functions/02-developing-in-Go.md
index 0cb28a74..b95a8eaa 100644
--- a/site/book/05-developing-functions/02-developing-in-Go.md
+++ b/site/book/05-developing-functions/02-developing-in-Go.md
@@ -1,5 +1,4 @@
-You can use the `kyaml` library to develop functions in Go. Doing so provides
-the following features:
+You can develop a KRM function in Go using [the kpt function SDK].
 
 - **General-purpose language:** Compared to Domain Specific Languages (DSL), Go
   is a general-purpose programming language that provides:
@@ -7,127 +6,131 @@ the following features:
   - An extensive ecosystem of tooling (e.g. IDE support)
   - A comprehensive catalog of well-supported libraries
   - Robust community support and detailed documentation
-- **YAML-centric**: As opposed to other frameworks discussed in this chapter,
-  the `kyaml` library exposes the YAML Abstract Syntax Tree (AST) to the user.
-  This enables you to control every aspect of the YAML file including
-  manipulating comments; however, it comes at the cost of complexity compared to
-  representing resources as idiomatic data structures.
+
+## Prerequisites
+
+- [Install kpt](https://kpt.dev/installation/)
+
+- [Install Docker](https://docs.docker.com/get-docker/)
 
 ## Quickstart
 
-### Create the go module
+In this quickstart, we will write a function that adds an annotation 
+`config.kubernetes.io/managed-by=kpt` to all `Deployment` resources.
+
+### Initialize your project
+
+We start from a "get-started" package which contains a `main.go` file with some scaffolding code.
 
 ```shell
-$ go mod init github.com/user/repo; go get sigs.k8s.io/kustomize/kyaml@v0.12.0
-```
+# Set your KRM function name.
+export FUNCTION_NAME=set-annotation
 
-### Create the `main.go`
+# Get the "get-started" package.
+kpt pkg get https://github.com/GoogleContainerTools/kpt-functions-sdk.git/go/get-started@master ${FUNCTION_NAME}
 
-This is a simple function that adds the annotation `myannotation` with the
-provided value:
+cd ${FUNCTION_NAME}
+
+# Initialize the Go module
+go mod init
+go mod tidy
+```
+
+### Write the KRM function logic
+ 
+Take a look at the `main.go` (as below) and complete the `Run` function.
 
 ```go
 // main.go
 package main
 
 import (
-	"os"
+  "fmt"
+  "os"
 
-	"sigs.k8s.io/kustomize/kyaml/fn/framework"
-	"sigs.k8s.io/kustomize/kyaml/fn/framework/command"
-	"sigs.k8s.io/kustomize/kyaml/kio"
-	"sigs.k8s.io/kustomize/kyaml/yaml"
+  "github.com/GoogleContainerTools/kpt-functions-sdk/go/fn"
 )
 
+// EDIT THIS FUNCTION!
+// This is the main logic. rl is the input `ResourceList` which has the `FunctionConfig` and `Items` fields.
+// You can modify the `Items` and add result information to `rl.Results`.
+func Run(rl *fn.ResourceList) (bool, error) {
+  // Your code
+}
+
 func main() {
-	// create a struct matching the structure of ResourceList.FunctionConfig to hold its data
-	var config struct {
-		Data map[string]string `yaml:"data"`
-	}
-	fn := func(items []*yaml.RNode) ([]*yaml.RNode, error) {
-		for i := range items {
-			// set the annotation on each resource item
-			err := items[i].PipeE(yaml.SetAnnotation("myannotation", config.Data["myannotation"]))
-			if err != nil {
-				return nil, err
-			}
-		}
-		return items, nil
-	}
-	p := framework.SimpleProcessor{Filter: kio.FilterFunc(fn), Config: &config}
-	cmd := command.Build(p, command.StandaloneDisabled, false)
-  // Adds a "gen" subcommand to create a Dockerfile for building the function into a container image.
-  command.AddGenerateDockerfile(cmd)
-
-	if err := cmd.Execute(); err != nil {
-		os.Exit(1)
-	}
+  // CUSTOMIZE IF NEEDED
+  // `AsMain` accepts a `ResourceListProcessor` interface.
+  // You can explore other `ResourceListProcessor` structs in the SDK or define your own.
+  if err := fn.AsMain(fn.ResourceListProcessorFunc(Run)); err != nil {
+    os.Exit(1)
+  }
 }
 ```
 
-### Build and test the function
+The [`fn`] library provides a series of KRM level operations for [`ResourceList`]. 
+Basically, the KRM resource `ResourceList.FunctionConfig` and KRM resources `ResourceList.Items` are both converted to 
+`KubeObject` objects. You can use `KubeObject` similar as [`unstructured.Unstrucutred`].
 
-Build the go binary:
+The set-annotation function (see below) iterates the `ResourceList.Items`, finds out the `Deployment` resources and
+adds the annotation. After the iteration, it adds some user message to the `ResourceList.Results`
 
-```shell
-$ go mod tidy; go build -o my-fn .
+```go
+func Run(rl *fn.ResourceList) (bool, error) {
+    for _, kubeObject := range rl.Items {
+        if kubeObject.IsGVK("v1", "Deployment") {
+            kubeObject.SetAnnotation("config.kubernetes.io/managed-by", "kpt")
+        }
+    }
+    // This result message will be displayed in the function evaluation time. 
+    rl.Results = append(rl.Results, fn.GeneralResult("Add config.kubernetes.io/managed-by=kpt to all `Deployment` resources", fn.Info))
+    return true, nil
+}
 ```
 
-Fetch the wordpress package:
+Learn more about the `KubeObject` from the [go doc](https://pkg.go.dev/github.com/GoogleContainerTools/kpt-functions-sdk/go/fn).
 
-```shell
-$ kpt pkg get https://github.com/GoogleContainerTools/kpt.git/package-examples/wordpress@v0.8
-```
 
-Test it by running the function imperatively:
+### Test the KRM function
 
-```shell
-$ kpt fn eval wordpress --exec ./my-fn -- myannotation=foo
-```
-
-During iterative development, `--exec` flag can be used to execute the
-function binary directly instead of requiring the function to be containerized
-first. Once you have a function binary that works, you can then proceed to
-creating the container image.
+The "get-started" package contains a `./data` directory. You can use this to test out your functions. 
 
-### Publish the function
+```shell
+# Edit the `data/resources.yaml` with your KRM resources. 
+# resources.yaml already has a `Deployment` and `Service` as test data. 
+vim data/resources.yaml
 
-Generate a Dockerfile for the function image:
+# Convert the KRM resources and FunctionConfig resource to `ResourceList`, and 
+# then pipe the ResourceList as StdIn to your function
+kpt fn source data | go run main.go
 
-```shell
-$ go run ./main.go gen ./
+# Verify the KRM function behavior in the StdOutput `ResourceList`
 ```
 
-Build the image:
+### Build the KRM function as a Docker image
 
-```shell
-$ docker build . -t gcr.io/project/fn-name:tag
-```
+Build the image
 
-Optionally, push the image to a container registry:
+The "get-started" package provides the `Dockerfile`.
 
 ```shell
-$ docker push gcr.io/project/fn-name:tag
+export FN_CONTAINER_REGISTRY=<Your GCR or docker hub>
+export TAG=<Your KRM function tag>
+docker build . -t ${FN_CONTAINER_REGISTRY}/${FUNCTION_NAME}:${TAG}
 ```
 
-Run the function imperatively as a container function:
-
+To verify the image using the same `./data` resources
 ```shell
-$ kpt fn eval wordpress -i gcr.io/project/fn-name:tag -- myannotation=foo
+kpt fn eval ./data --image ${FN_CONTAINER_REGISTRY}/${FUNCTION_NAME}:${TAG}
 ```
 
 ## Next Steps
 
-- Read the package documentation:
-
-| Package                                    | Purpose               |
-| ------------------------------------------ | --------------------- |
-| [sigs.k8s.io/kustomize/kyaml/fn/framework] | Functions Framework   |
-| [sigs.k8s.io/kustomize/kyaml/yaml]         | Modify YAML resources |
-
-- Take a look at the source code for [functions in the catalog] to better
-  understand how to develop functions in Go
+- See other [go doc examples] to use KubeObject.
+- To contribute to KRM catalog functions, please follow the [contributor guide](https://github.com/GoogleContainerTools/kpt-functions-catalog/blob/master/CONTRIBUTING.md)
 
-[sigs.k8s.io/kustomize/kyaml/fn/framework]: https://pkg.go.dev/sigs.k8s.io/kustomize/kyaml@v0.11.1/fn/framework#pkg-index
-[sigs.k8s.io/kustomize/kyaml/yaml]: https://pkg.go.dev/sigs.k8s.io/kustomize/kyaml@v0.11.1/yaml
-[functions in the catalog]: https://github.com/GoogleContainerTools/kpt-functions-catalog/tree/master/functions/go
+[the kpt function SDK]: https://pkg.go.dev/github.com/GoogleContainerTools/kpt-functions-sdk/go/fn
+[go doc examples]: https://pkg.go.dev/github.com/GoogleContainerTools/kpt-functions-sdk/go/fn/examples
+[`fn`]: https://pkg.go.dev/github.com/GoogleContainerTools/kpt-functions-sdk/go/fn
+[`ResourceList`]: https://github.com/kubernetes-sigs/kustomize/blob/master/cmd/config/docs/api-conventions/functions-spec.md
+[`unstructured.Unstrucutred`]: https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured
diff --git a/site/book/05-developing-functions/04-developing-in-Starlark.md b/site/book/05-developing-functions/04-developing-in-Starlark.md
new file mode 100644
index 00000000..6bfaf420
--- /dev/null
+++ b/site/book/05-developing-functions/04-developing-in-Starlark.md
@@ -0,0 +1,58 @@
+You can write the function in Starlark script.
+
+?> Starlark SDK is in *experimental* stage.
+
+[Starlark] is a python-like language designed for use in configuration files that has several desirable properties:
+* deterministic evaluation
+* hermetic execution 
+* simplicity.
+
+Current Starlark SDK is driven by [`gcr.io/kpt-fn/starlark:v0.4`] which contains the interpreter and accepts 
+a `StarlarkRun` object as its `FunctionConfig`. You should place your starlark script in the `source` field
+of the `StarlarkRun` object. 
+
+## Quickstart
+
+Let's write a starlark function which add annotation "managed-by=kpt" only to `Deployment` resources.
+
+### Get the "get-started" example
+
+```shell
+kpt pkg get https://github.com/GoogleContainerTools/kpt-functions-sdk.git/starlark/get-started@master set-annotation
+cd set-annotation
+```
+
+### Update the `FunctionConfig`
+```yaml
+# starlark-fn-config.yaml
+apiVersion: fn.kpt.dev/v1alpha1
+kind: StarlarkRun
+metadata:
+  name: set-annotation
+# EDIT THE SOURCE! 
+# This should be your starlark script which preloads the `ResourceList` to `ctx.resource_list`
+source: |
+  for resource in ctx.resource_list["items"]:
+    if resource.get("kind") == "Deployment":
+      resource["metadata"]["annotations"]["managed-by"] = "kpt"
+```
+In the `source` field, the `ResourceList` from STDIN is loaded to `ctx.resource_list` as a dict. 
+You can manipulate KRM resource as operating on a dict.
+
+### Test and Run
+
+Run the starlark script via `kpt`   
+```shell
+# `starlark:v0.4` is the short form of gcr.io/kpt-fn/starlark:v0.4 catalog function. 
+kpt fn eval ./data --image starlark:v0.4 --fn-config starlark-fn-config.yaml
+
+# Verify that the annotation is added to the `Deployment` resource and the other resource `Service` 
+# does not have this annotation.
+cat ./data/resources.yaml | grep annotations -A1 -B5
+```
+
+?> Refer to the [Functions Catalog](https://catalog.kpt.dev/starlark/v0.4/) for
+details on how to use this function.
+
+[`gcr.io/kpt-fn/starlark:v0.4`]: https://catalog.kpt.dev/starlark/v0.4/
+[Starlark]: https://github.com/bazelbuild/starlark#starlark
\ No newline at end of file
diff --git a/site/book/05-developing-functions/04-executable-configuration.md b/site/book/05-developing-functions/04-executable-configuration.md
deleted file mode 100644
index 604026e6..00000000
--- a/site/book/05-developing-functions/04-executable-configuration.md
+++ /dev/null
@@ -1,128 +0,0 @@
-In this section we are going to take a look at two functions that follow the
-[executable configuration pattern] we discussed at the beginning of this
-chapter: `starlark` and `gatekeeper`.
-
-## `starlark` function
-
-[Starlark] is a python-like language designed for use in configuration files
-that has several desirable properties: deterministic evaluation, hermetic
-execution, and simplicity. The `starlark` function contains the interpreter for
-the language, and accepts a `functionConfig` of kind `StarlarkRun` where you
-provide your business logic.
-
-The following is an example package with a `starlark` function declaration:
-
-```shell
-$ kpt pkg get https://github.com/GoogleContainerTools/kpt-functions-catalog.git/examples/starlark-set-namespace@starlark/v0.2
-```
-
-It contains the following `functionConfig`:
-
-```yaml
-# set-namespace/fn-config.yaml
-apiVersion: fn.kpt.dev/v1alpha1
-kind: StarlarkRun
-metadata:
-  name: set-namespace-to-prod
-source: |
-  # set the namespace on all resources
-  def setnamespace(resources, namespace):
-    for resource in resources:
-      # mutate the resource
-      resource["metadata"]["namespace"] = namespace
-  setnamespace(ctx.resource_list["items"], "prod")
-```
-
-The `source` field includes the Starlark logic that sets the namespace on all
-resources. Go ahead and render the package:
-
-```shell
-$ kpt fn render set-namespace
-```
-
-You should now see that resources have `namespace` set to `prod`.
-
-?> Refer to the [Functions Catalog](https://catalog.kpt.dev/starlark/v0.2/) for
-details on how use this function.
-
-## `gatekeeper` function
-
-[Gatekeeper] project provides an extensible policy enforcement framework which
-can be deployed as a Kubernetes admission controller (admission-time
-enforcement) or as a kpt function (configuration authoring-time enforcement).
-Policies are authored using custom resources that includes the business logic in
-[Rego], a language designed for expressing policies over complex hierarchical
-data structures.
-
-The following is an example package with a `gatekeeper` function declaration:
-
-```shell
-$ kpt pkg get https://github.com/GoogleContainerTools/kpt-functions-catalog.git/examples/gatekeeper-invalid-configmap@gatekeeper/v0.1
-```
-
-It contains the policy constraint containing the Rego logic which looks for
-banned fields in `ConfigMap` which may contain credentials which you do not want
-declared in a package and committed to Git:
-
-```yaml
-# invalid-configmap/policy.yaml
-apiVersion: templates.gatekeeper.sh/v1beta1
-kind: ConstraintTemplate
-metadata: # kpt-merge: /k8sbannedconfigmapkeysv1
-  name: k8sbannedconfigmapkeysv1
-spec:
-  crd:
-    spec:
-      names:
-        kind: K8sBannedConfigMapKeysV1
-        validation:
-          openAPIV3Schema:
-            properties:
-              keys:
-                type: array
-                items:
-                  type: string
-  targets:
-    - target: admission.k8s.gatekeeper.sh
-      rego: |-
-        package ban_keys
-
-        violation[{"msg": sprintf("%v", [val])}] {
-          keys = {key | input.review.object.data[key]}
-          banned = {key | input.parameters.keys[_] = key}
-          overlap = keys & banned
-          count(overlap) > 0
-          val := sprintf("The following banned keys are being used in the ConfigMap: %v", [overlap])
-        }
----
-apiVersion: constraints.gatekeeper.sh/v1beta1
-kind: K8sBannedConfigMapKeysV1
-metadata: # kpt-merge: /no-secrets-in-configmap
-  name: no-secrets-in-configmap
-spec:
-  match:
-    kinds:
-      - apiGroups:
-          - ""
-        kinds:
-          - ConfigMap
-  parameters:
-    keys:
-      - private_key
-```
-
-If you render the package, you will get a validation error as expected:
-
-```shell
-$ kpt fn render invalid-configmap
-```
-
-?> Refer to the
-[Functions Catalog](https://catalog.kpt.dev/gatekeeper/v0.1/) for
-details on how use this function.
-
-[executable configuration pattern]:
-  /book/05-developing-functions/?id=executable-configuration
-[starlark]: https://github.com/bazelbuild/starlark#starlark
-[gatekeeper]: https://github.com/open-policy-agent/gatekeeper#gatekeeper
-[rego]: https://www.openpolicyagent.org/docs/latest/#rego
diff --git a/site/coverpage.md b/site/coverpage.md
index d24ac3fe..e4eb6baf 100644
--- a/site/coverpage.md
+++ b/site/coverpage.md
@@ -6,8 +6,8 @@
 > by manipulating declarative Configuration as Data, 
 > separated from the code that transforms it.
 
-<iframe id="ytplayer" type="text/html" width="550" height="360"
-    src="https://www.youtube.com/embed/L_x7z4CXHDw" allowFullScreen="allowFullScreen"> </iframe>
+<iframe id="ytplayer" type="text/html" width="330" height="216"
+    src="https://www.youtube.com/embed/L_x7z4CXHDw" align="left" allowFullScreen="allowFullScreen"> </iframe>
 
 
 [Why kpt](guides/rationale.md)
diff --git a/site/guides/README.md b/site/guides/README.md
index 573e2ffd..dcd63cb1 100644
--- a/site/guides/README.md
+++ b/site/guides/README.md
@@ -1,10 +1,10 @@
 # Reference
 
-- [The Rationale behind kpt](guides/rationale.md)
-- [Namespace provisioning CLI](guides/namespace-provisioning-cli.md)
-- [Namespace provisioning UI](guides/namespace-provisioning-ui.md)
+- [The Rationale Behind kpt](guides/rationale.md)
 - [Porch Installation Guide](guides/porch-installation.md)
+- [Porch UI Installation Guide](guides/porch-ui-installation.md)
 - [Porch User Guide](guides/porch-user-guide.md)
-- [Tenant Onboarding Guide](guides/tenant-onboarding.md)
+- [Namespace Provisioning CLI](guides/namespace-provisioning-cli.md)
+- [Namespace Provisioning UI](guides/namespace-provisioning-ui.md)
 - [Variant Constructor Pattern](guides/variant-constructor-pattern.md)
-- [Value Propagation Pattern](guides/value-propagation.md)
+- [Value Propagation Pattern](guides/value-propagation.md)
\ No newline at end of file
diff --git a/site/guides/namespace-provisioning-cli.md b/site/guides/namespace-provisioning-cli.md
index 77665cbf..add44611 100644
--- a/site/guides/namespace-provisioning-cli.md
+++ b/site/guides/namespace-provisioning-cli.md
@@ -16,8 +16,8 @@ In this guide, we will create a package that will be used for provisioning a nam
 ### Repositories
 
 Platform teams will have to setup two repos:
-1.Blueprint repo where reusable kpt packages will live.
-2.Deployment repo where instances of packages that will be deployed to a
+1. Blueprint repo where reusable kpt packages will live.
+2. Deployment repo where instances of packages that will be deployed to a
 kubernetes cluster will live. Users will create deployable packages from the
 packages in the blueprint repo.
 
diff --git a/site/guides/porch-installation.md b/site/guides/porch-installation.md
index 1d90f1c2..7a2ee736 100644
--- a/site/guides/porch-installation.md
+++ b/site/guides/porch-installation.md
@@ -69,7 +69,7 @@ To run a released version of Porch, download the release config bundle from
 Untar and applly the config bundle. This will install:
 
 * Porch server
-* [Config Sync](https://cloud.google.com/anthos-config-management/docs/config-sync-overview)
+* [Config Sync](https://kpt.dev/gitops/configsync/)
 
 ```sh
 mkdir porch-install
@@ -125,7 +125,7 @@ for details).
 
 The `make push-and-deploy-no-sa` target will install Porch but not Config Sync.
 You can install Config Sync in your k8s cluster manually following the
-[documentation](https://cloud.google.com/anthos-config-management/docs/how-to/installing-kubectl).
+[documentation](https://github.com/GoogleContainerTools/kpt-config-sync/blob/main/docs/installation.md).
 
 **Note**: The `-no-sa` (no service account) targets create Porch deployment
 configuration which does not associate Kubernetes service accounts with GCP
@@ -192,30 +192,3 @@ kubectl api-resources | grep porch
 
 And start [using Porch](./porch-user-guide.md) if the Porch resources are
 available.
-
-## Deploying Configuration
-
-**Note:** Rather than using Config Sync, this section uses a prototype sync
-controller.
-
-Create some example repositories / packages:
-
-```sh
-# Create artifact-registry repos etc
-make apply-dev-config
-# Push a sample hello-world app
-make -C config/samples/apps/hello-server push-image
-# Create a package for the sample hello-world app
-./scripts/create-deployment-package.sh
-```
-
-To test out remoterootsync self-applying:
-
-```sh
-# Grant more RBAC permissions than are normally needed (equivalent to admin permissions)
-kubectl apply -f controllers/remoterootsync/config/samples/hack-self-apply-rbac.yaml
-
-# Apply the RemoteRootSyncSet
-cat controllers/remoterootsync/config/samples/hack-self-apply.yaml \
-  | sed -e s/example-google-project-id/${GCP_PROJECT_ID}/g | kubectl apply -f -
-```
diff --git a/site/guides/porch-user-guide.md b/site/guides/porch-user-guide.md
index c72a5d52..fbf8ed27 100644
--- a/site/guides/porch-user-guide.md
+++ b/site/guides/porch-user-guide.md
@@ -15,8 +15,8 @@ To use Porch, you will need:
 
 Make sure that your `kubectl` context is set up for `kubectl` to interact with the
 correct Kubernetes instance (see
-[installation instructions](../site/guides/porch-installation.md) or
-the [running-locally](./docs/running-locally.md) guide for details).
+[installation instructions](./porch-installation.md) or
+the [running-locally](https://github.com/GoogleContainerTools/kpt/blob/main/porch/docs/running-locally.md) guide for details).
 
 To check whether `kubectl` is configured with your Porch cluster (or local instance), run:
 
@@ -477,8 +477,18 @@ actuation target as a means of deployment) and include:
 * `del` - Deletes the package RootSync.
 * `get` - Gets a RootSync resource with which package was deployed.
 
-
 ```sh
+# Make sure Config Sync is configured to use multirepo mode
+kubectl apply -f - <<EOF
+# config-management.yaml
+apiVersion: configmanagement.gke.io/v1
+kind: ConfigManagement
+metadata:
+  name: config-management
+spec:
+  enableMultiRepo: true
+EOF
+
 # Create a sync resource to deploy a package using Config Sync
 $ kpt alpha sync create -ndefault \
   --package=deployments-11ca1db650fa4bfa33deeb7f488fbdc50cdb3b82 \
@@ -487,7 +497,7 @@ $ kpt alpha sync create -ndefault \
 Created RootSync config-management-system/sync-istions-clone
 
 # Get the status of the sync resource
-$ kpt alpha sync get -nconfig-management-system sync-istions-clone -oyaml
+$ kpt alpha sync get sync-istions-clone -oyaml
 apiVersion: configsync.gke.io/v1beta1
 kind: RootSync
 metadata:
@@ -496,8 +506,8 @@ metadata:
 ...
 
 # Delete the sync resource
-$ kpt alpha sync delete -nconfig-management-system sync-istions-clone
+$ kpt alpha sync delete sync-istions-clone
 Deleting synced resources
 Waiting for deleted resources to be removed
 Sync sync-istions-clone successfully deleted
-```
\ No newline at end of file
+```
diff --git a/site/guides/rationale.md b/site/guides/rationale.md
index 43421e81..a9654081 100644
--- a/site/guides/rationale.md
+++ b/site/guides/rationale.md
@@ -14,7 +14,7 @@ It is an emerging trend to provide GUIs over GitOps, but their ability to automa
 
 Fortunately, code-like representations are not a requirement in order to represent the desired state declaratively. The Kubernetes API was [designed to be natively declarative](https://github.com/kubernetes/design-proposals-archive/blob/main/architecture/resource-management.md#declarative-configuration). The serialized configuration format of resources is [identical to their API wire format](https://github.com/kubernetes/design-proposals-archive/blob/main/architecture/declarative-application-management.md#configuration-using-rest-api-resource-specifications). These resource representations were intended to form the core of a declarative data model, which is sufficient in order to support pre-deployment validation, preview, and approval and post-deployment auditing, versioning, and undo. 
 
-kpt supports management of [Configuration as Data](../../docs/design-docs/06-config-as-data.md). The core ideas are simple:
+kpt supports management of [Configuration as Data](https://github.com/GoogleContainerTools/kpt/blob/main/docs/design-docs/06-config-as-data.md). The core ideas are simple:
 * uses a uniform, serializable data model to represent configuration ([KRM](https://github.com/kubernetes/design-proposals-archive/blob/main/architecture/resource-management.md))
 * makes configuration data (packages) the source of truth, stored separately from the live state 
 * separates code that acts on the configuration (functions) from the configuration data
@@ -22,7 +22,7 @@ kpt supports management of [Configuration as Data](../../docs/design-docs/06-con
 
 kpt builds on our learnings from [Kustomize](https://kubernetes-sigs.github.io/kustomize/). Like kustomize, kpt uses a transformation-based approach, but optimizes for in-place configuration transformation rather than out-of-place transformation. As with typical API clients, this enables interoperability of a variety of generators, transformers, and validators. One doesn't need to make all changes through a monolithic generator implementation. 
 
-kpt also extends its capabilities in areas that are [out of scope](https://github.com/kubernetes/design-proposals-archive/blob/main/architecture/scope.md#examples-of-projects-and-areas-not-in-scope), notably packaging, and provides a [package orchestration service](../../docs/design-docs/07-package-orchestration.md) in addition to a client-side CLI.
+kpt also extends its capabilities in areas that are [out of scope](https://github.com/kubernetes/design-proposals-archive/blob/main/architecture/scope.md#examples-of-projects-and-areas-not-in-scope), notably packaging, and provides a [package orchestration service](https://github.com/GoogleContainerTools/kpt/blob/main/docs/design-docs/07-package-orchestration.md) in addition to a client-side CLI.
 
 kpt enables WYSIWYG management of configuration similar to how the live state can be modified with traditional imperative tools, thus eliminating this dichotomy:
 <img src="https://raw.githubusercontent.com/GoogleContainerTools/kpt/main/docs/design-docs/CaD%20Overview.svg">
diff --git a/site/guides/value-propagation.md b/site/guides/value-propagation.md
index 4a8ed2a4..ffe133f6 100644
--- a/site/guides/value-propagation.md
+++ b/site/guides/value-propagation.md
@@ -1,7 +1,7 @@
 [starlark]: https://catalog.kpt.dev/starlark/v0.4/
 [apply-replacements]: https://catalog.kpt.dev/apply-replacements/v0.1/
 
-# Value propagation
+# Value propagation pattern
 
 Generating a string value and propagating that value to another place
 (or many other places) in your configuration is a very common pattern. 
diff --git a/site/sidebar.md b/site/sidebar.md
index 009f2683..6fc8da1c 100644
--- a/site/sidebar.md
+++ b/site/sidebar.md
@@ -25,7 +25,7 @@
 		- [5.1 Functions Specification](book/05-developing-functions/01-functions-specification.md)
 		- [5.2 Developing in Go](book/05-developing-functions/02-developing-in-Go.md)
 		- [5.3 Developing in Typescript](book/05-developing-functions/03-developing-in-Typescript.md)
-		- [5.4 Executable Configuration](book/05-developing-functions/04-executable-configuration.md)
+		- [5.4 Developing in Starlark](book/05-developing-functions/04-developing-in-Starlark.md)
 	- [6 Deploying Packages](book/06-deploying-packages/)
 		- [6.1 Initializing a Package for Apply](book/06-deploying-packages/01-initializing-a-package-for-apply.md)
 		- [6.2 Applying a Package](book/06-deploying-packages/02-applying-a-package.md)
@@ -96,11 +96,12 @@
 - [GitOps](gitops/)
   - [Config Sync](gitops/configsync/)
 - [Guides](guides/)
-  - [The Rationale behind kpt](guides/rationale.md)
-  - [Namespace provisioning CLI](guides/namespace-provisioning-cli.md)
-  - [Namespace provisioning UI](guides/namespace-provisioning-ui.md)
+  - [The Rationale Behind kpt](guides/rationale.md)
   - [Porch Installation Guide](guides/porch-installation.md)
+  - [Porch UI Installation Guide](guides/porch-ui-installation.md)
   - [Porch User Guide](guides/porch-user-guide.md)
+  - [Namespace Provisioning CLI](guides/namespace-provisioning-cli.md)
+  - [Namespace Provisioning UI](guides/namespace-provisioning-ui.md)
   - [Variant Constructor Pattern](guides/variant-constructor-pattern.md)
   - [Value Propagation Pattern](guides/value-propagation.md)
 - [FAQ](faq/)
diff --git a/site/sitemap.xml b/site/sitemap.xml
index f6450da8..cd645a04 100644
--- a/site/sitemap.xml
+++ b/site/sitemap.xml
@@ -109,7 +109,7 @@
     <lastmod>2022-05-01</lastmod>
   </url>
   <url>
-    <loc>https://kpt.dev/book/05-developing-functions/04-executable-configuration</loc>
+    <loc>https://kpt.dev/book/05-developing-functions/04-developing-in-Starlark</loc>
     <lastmod>2022-05-01</lastmod>
   </url>
   <url>


Test patch:
diff --git a/e2e/live_test.go b/e2e/live_test.go
index 159c1e14..c7944046 100644
--- a/e2e/live_test.go
+++ b/e2e/live_test.go
@@ -1,3 +1,4 @@
+//go:build kind
 // +build kind
 
 // Copyright 2021 Google LLC
@@ -29,6 +30,10 @@ func TestLiveApply(t *testing.T) {
 	runTests(t, filepath.Join(".", "testdata", "live-apply"))
 }
 
+func TestLivePlan(t *testing.T) {
+	runTests(t, filepath.Join(".", "testdata", "live-plan"))
+}
+
 func runTests(t *testing.T, path string) {
 	testCases := scanTestCases(t, path)
 
@@ -62,8 +67,8 @@ func runTests(t *testing.T, path string) {
 			defer livetest.RemoveNamespace(t, ns)
 
 			(&livetest.Runner{
-				Config:    c,
-				Path:      p,
+				Config: c,
+				Path:   p,
 			}).Run(t)
 		})
 	}
diff --git a/e2e/testdata/live-apply/apply-depends-on/config.yaml b/e2e/testdata/live-apply/apply-depends-on/config.yaml
index b9549880..e2e0e020 100644
--- a/e2e/testdata/live-apply/apply-depends-on/config.yaml
+++ b/e2e/testdata/live-apply/apply-depends-on/config.yaml
@@ -15,6 +15,8 @@
 parallel: true
 
 kptArgs:
+  - "live"
+  - "apply"
   - "--reconcile-timeout=2m"
 
 stdOut: |
diff --git a/e2e/testdata/live-apply/crd-and-cr/config.yaml b/e2e/testdata/live-apply/crd-and-cr/config.yaml
index a0e5ef4a..38e5c621 100644
--- a/e2e/testdata/live-apply/crd-and-cr/config.yaml
+++ b/e2e/testdata/live-apply/crd-and-cr/config.yaml
@@ -15,6 +15,8 @@
 parallel: true
 
 kptArgs:
+  - "live"
+  - "apply"
   - "--reconcile-timeout=1m"
 
 stdOut: |
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 add5fb5e..a79fb851 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
@@ -15,6 +15,8 @@
 parallel: false
 noResourceGroup: true
 kptArgs:
+  - "live"
+  - "apply"
   - "--dry-run"
   - "--install-resource-group"
 stdOut: |
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 608a7e83..a45ee3c6 100644
--- a/e2e/testdata/live-apply/dry-run-without-rg/config.yaml
+++ b/e2e/testdata/live-apply/dry-run-without-rg/config.yaml
@@ -15,6 +15,8 @@
 parallel: false
 noResourceGroup: true
 kptArgs:
+  - "live"
+  - "apply"
   - "--dry-run"
 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.
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 b0019b96..22479834 100644
--- a/e2e/testdata/live-apply/install-rg-on-apply/config.yaml
+++ b/e2e/testdata/live-apply/install-rg-on-apply/config.yaml
@@ -15,6 +15,8 @@
 parallel: false
 noResourceGroup: true
 kptArgs:
+  - "live"
+  - "apply"
   - "--reconcile-timeout=1m"
 stdOut: |
   deployment.apps/nginx-deployment created
diff --git a/e2e/testdata/live-apply/json-output/config.yaml b/e2e/testdata/live-apply/json-output/config.yaml
index 2a5526d4..45c2b3f4 100644
--- a/e2e/testdata/live-apply/json-output/config.yaml
+++ b/e2e/testdata/live-apply/json-output/config.yaml
@@ -14,6 +14,8 @@
 
 parallel: true
 kptArgs:
+  - "live"
+  - "apply"
   - "--output=json"
   - "--reconcile-timeout=2m"
 stdOut: |
diff --git a/e2e/testdata/live-apply/prune-depends-on/config.yaml b/e2e/testdata/live-apply/prune-depends-on/config.yaml
index 2f85051b..6b46a4fa 100644
--- a/e2e/testdata/live-apply/prune-depends-on/config.yaml
+++ b/e2e/testdata/live-apply/prune-depends-on/config.yaml
@@ -15,6 +15,8 @@
 parallel: true
 
 kptArgs:
+  - "live"
+  - "apply"
   - "--reconcile-timeout=1m"
 
 stdOut: |
diff --git a/e2e/testdata/live-plan/create/config.yaml b/e2e/testdata/live-plan/create/config.yaml
new file mode 100644
index 00000000..c0b38824
--- /dev/null
+++ b/e2e/testdata/live-plan/create/config.yaml
@@ -0,0 +1,61 @@
+# 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:
+  - "live"
+  - "plan"
+
+stdOut: |
+  apiVersion: config.kubernetes.io/v1
+  kind: ResourceList
+  items:
+  - apiVersion: config.google.com/v1alpha1
+    kind: Plan
+    metadata:
+      name: plan
+    spec:
+      actions:
+      - action: ApplyCreate
+        kind: ConfigMap
+        name: foo
+        namespace: create
+        after:
+          object:
+            apiVersion: v1
+            data:
+              key: value
+            kind: ConfigMap
+            metadata:
+              annotations:
+                config.k8s.io/owning-inventory: create
+              creationTimestamp: "<TIMESTAMP>"
+              managedFields:
+              - apiVersion: v1
+                fieldsType: FieldsV1
+                fieldsV1:
+                  f:data:
+                    f:key: {}
+                  f:metadata:
+                    f:annotations:
+                      f:config.k8s.io/owning-inventory: {}
+                manager: kubectl
+                operation: Apply
+                time: "<TIMESTAMP>"
+              name: foo
+              namespace: create
+              uid: <UID>
+
+
diff --git a/e2e/testdata/live-plan/create/resources/Kptfile b/e2e/testdata/live-plan/create/resources/Kptfile
new file mode 100644
index 00000000..946835b4
--- /dev/null
+++ b/e2e/testdata/live-plan/create/resources/Kptfile
@@ -0,0 +1,8 @@
+apiVersion: kpt.dev/v1
+kind: Kptfile
+metadata:
+  name: create
+inventory:
+  namespace: create
+  name: create
+  inventoryID: create
diff --git a/e2e/testdata/live-plan/create/resources/cm.yaml b/e2e/testdata/live-plan/create/resources/cm.yaml
new file mode 100644
index 00000000..dd896ead
--- /dev/null
+++ b/e2e/testdata/live-plan/create/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: foo
+  namespace: create
+data:
+  key: value
\ No newline at end of file
diff --git a/e2e/testdata/live-plan/prune/config.yaml b/e2e/testdata/live-plan/prune/config.yaml
new file mode 100644
index 00000000..f3a2fc06
--- /dev/null
+++ b/e2e/testdata/live-plan/prune/config.yaml
@@ -0,0 +1,210 @@
+# 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:
+  - "live"
+  - "plan"
+
+stdOut: |
+  apiVersion: config.kubernetes.io/v1
+  kind: ResourceList
+  items:
+  - apiVersion: config.google.com/v1alpha1
+    kind: Plan
+    metadata:
+      name: plan
+    spec:
+      actions:
+      - action: ApplyChanges
+        kind: ConfigMap
+        name: cm
+        namespace: prune
+        before:
+          object:
+            apiVersion: v1
+            data:
+              key: value
+            kind: ConfigMap
+            metadata:
+              annotations:
+                config.k8s.io/owning-inventory: prune
+                kubectl.kubernetes.io/last-applied-configuration: |
+                  {"apiVersion":"v1","data":{"key":"value"},"kind":"ConfigMap","metadata":{"annotations":{"config.k8s.io/owning-inventory":"prune"},"name":"cm","namespace":"prune"}}
+              creationTimestamp: "<TIMESTAMP>"
+              managedFields:
+              - apiVersion: v1
+                fieldsType: FieldsV1
+                fieldsV1:
+                  f:data:
+                    .: {}
+                    f:key: {}
+                  f:metadata:
+                    f:annotations:
+                      .: {}
+                      f:config.k8s.io/owning-inventory: {}
+                      f:kubectl.kubernetes.io/last-applied-configuration: {}
+                manager: kubectl-client-side-apply
+                operation: Update
+                time: "<TIMESTAMP>"
+              name: cm
+              namespace: prune
+              resourceVersion: "<RV>"
+              uid: <UID>
+        after:
+          object:
+            apiVersion: v1
+            data:
+              key: value
+            kind: ConfigMap
+            metadata:
+              annotations:
+                config.k8s.io/owning-inventory: prune
+                kubectl.kubernetes.io/last-applied-configuration: |
+                  {"apiVersion":"v1","data":{"key":"value"},"kind":"ConfigMap","metadata":{"annotations":{"config.k8s.io/owning-inventory":"prune"},"name":"cm","namespace":"prune"}}
+              creationTimestamp: "<TIMESTAMP>"
+              managedFields:
+              - apiVersion: v1
+                fieldsType: FieldsV1
+                fieldsV1:
+                  f:data:
+                    f:key: {}
+                  f:metadata:
+                    f:annotations:
+                      f:config.k8s.io/owning-inventory: {}
+                manager: kubectl
+                operation: Apply
+                time: "<TIMESTAMP>"
+              - apiVersion: v1
+                fieldsType: FieldsV1
+                fieldsV1:
+                  f:data:
+                    .: {}
+                    f:key: {}
+                  f:metadata:
+                    f:annotations:
+                      .: {}
+                      f:config.k8s.io/owning-inventory: {}
+                      f:kubectl.kubernetes.io/last-applied-configuration: {}
+                manager: kubectl-client-side-apply
+                operation: Update
+                time: "<TIMESTAMP>"
+              name: cm
+              namespace: prune
+              resourceVersion: "<RV>"
+              uid: <UID>
+      - action: PrunePruned
+        apiVersion: apps
+        kind: Deployment
+        name: dep
+        namespace: prune
+        before:
+          object:
+            apiVersion: apps/v1
+            kind: Deployment
+            metadata:
+              annotations:
+                config.k8s.io/owning-inventory: prune
+                kubectl.kubernetes.io/last-applied-configuration: |
+                  {"apiVersion":"apps/v1","kind":"Deployment","metadata":{"annotations":{"config.k8s.io/owning-inventory":"prune"},"name":"dep","namespace":"prune"},"spec":{"replicas":1,"selector":{"matchLabels":{"app":"dep"}},"template":{"metadata":{"labels":{"app":"dep"}},"spec":{"containers":[{"image":"nginx:1.14.2","name":"nginx","ports":[{"containerPort":80}]}]}}}}
+              creationTimestamp: "<TIMESTAMP>"
+              generation: 1
+              managedFields:
+              - apiVersion: apps/v1
+                fieldsType: FieldsV1
+                fieldsV1:
+                  f:metadata:
+                    f:annotations:
+                      .: {}
+                      f:config.k8s.io/owning-inventory: {}
+                      f:kubectl.kubernetes.io/last-applied-configuration: {}
+                  f:spec:
+                    f:progressDeadlineSeconds: {}
+                    f:replicas: {}
+                    f:revisionHistoryLimit: {}
+                    f:selector: {}
+                    f:strategy:
+                      f:rollingUpdate:
+                        .: {}
+                        f:maxSurge: {}
+                        f:maxUnavailable: {}
+                      f:type: {}
+                    f:template:
+                      f:metadata:
+                        f:labels:
+                          .: {}
+                          f:app: {}
+                      f:spec:
+                        f:containers:
+                          k:{"name":"nginx"}:
+                            .: {}
+                            f:image: {}
+                            f:imagePullPolicy: {}
+                            f:name: {}
+                            f:ports:
+                              .: {}
+                              k:{"containerPort":80,"protocol":"TCP"}:
+                                .: {}
+                                f:containerPort: {}
+                                f:protocol: {}
+                            f:resources: {}
+                            f:terminationMessagePath: {}
+                            f:terminationMessagePolicy: {}
+                        f:dnsPolicy: {}
+                        f:restartPolicy: {}
+                        f:schedulerName: {}
+                        f:securityContext: {}
+                        f:terminationGracePeriodSeconds: {}
+                manager: kubectl-client-side-apply
+                operation: Update
+                time: "<TIMESTAMP>"
+              name: dep
+              namespace: prune
+              resourceVersion: "<RV>"
+              uid: <UID>
+            spec:
+              progressDeadlineSeconds: 600
+              replicas: 1
+              revisionHistoryLimit: 10
+              selector:
+                matchLabels:
+                  app: dep
+              strategy:
+                rollingUpdate:
+                  maxSurge: 25%
+                  maxUnavailable: 25%
+                type: RollingUpdate
+              template:
+                metadata:
+                  creationTimestamp: null
+                  labels:
+                    app: dep
+                spec:
+                  containers:
+                  - image: nginx:1.14.2
+                    imagePullPolicy: IfNotPresent
+                    name: nginx
+                    ports:
+                    - containerPort: 80
+                      protocol: TCP
+                    resources: {}
+                    terminationMessagePath: /dev/termination-log
+                    terminationMessagePolicy: File
+                  dnsPolicy: ClusterFirst
+                  restartPolicy: Always
+                  schedulerName: default-scheduler
+                  securityContext: {}
+                  terminationGracePeriodSeconds: 30
+            status: {}
diff --git a/e2e/testdata/live-plan/prune/pre-apply/cm.yaml b/e2e/testdata/live-plan/prune/pre-apply/cm.yaml
new file mode 100644
index 00000000..fcc5a09e
--- /dev/null
+++ b/e2e/testdata/live-plan/prune/pre-apply/cm.yaml
@@ -0,0 +1,23 @@
+# 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: prune
+  annotations:
+    config.k8s.io/owning-inventory: prune
+data:
+  key: value
\ No newline at end of file
diff --git a/e2e/testdata/live-plan/prune/pre-apply/dep.yaml b/e2e/testdata/live-plan/prune/pre-apply/dep.yaml
new file mode 100644
index 00000000..64d93c2d
--- /dev/null
+++ b/e2e/testdata/live-plan/prune/pre-apply/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: dep
+  namespace: prune
+  annotations:
+    config.k8s.io/owning-inventory: prune
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: dep
+  template:
+    metadata:
+      labels:
+        app: dep
+    spec:
+      containers:
+        - name: nginx
+          image: nginx:1.14.2
+          ports:
+            - containerPort: 80
\ No newline at end of file
diff --git a/e2e/testdata/live-plan/prune/pre-apply/rg.yaml b/e2e/testdata/live-plan/prune/pre-apply/rg.yaml
new file mode 100644
index 00000000..40ae15e1
--- /dev/null
+++ b/e2e/testdata/live-plan/prune/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: prune
+  name: prune
+  namespace: prune
+spec:
+  resources:
+    - group: ""
+      kind: ConfigMap
+      name: cm
+      namespace: prune
+    - group: apps
+      kind: Deployment
+      name: dep
+      namespace: prune
\ No newline at end of file
diff --git a/e2e/testdata/live-plan/prune/resources/Kptfile b/e2e/testdata/live-plan/prune/resources/Kptfile
new file mode 100644
index 00000000..b29cf046
--- /dev/null
+++ b/e2e/testdata/live-plan/prune/resources/Kptfile
@@ -0,0 +1,8 @@
+apiVersion: kpt.dev/v1
+kind: Kptfile
+metadata:
+  name: prune
+inventory:
+  namespace: prune
+  name: prune
+  inventoryID: prune
\ No newline at end of file
diff --git a/e2e/testdata/live-plan/prune/resources/cm.yaml b/e2e/testdata/live-plan/prune/resources/cm.yaml
new file mode 100644
index 00000000..608ce9d9
--- /dev/null
+++ b/e2e/testdata/live-plan/prune/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: prune
+data:
+  key: value
\ No newline at end of file
diff --git a/pkg/live/planner/cluster_test.go b/pkg/live/planner/cluster_test.go
new file mode 100644
index 00000000..5b48564d
--- /dev/null
+++ b/pkg/live/planner/cluster_test.go
@@ -0,0 +1,172 @@
+// Copyright 2022 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 planner
+
+import (
+	"context"
+	"testing"
+
+	planv1alpha1 "github.com/GoogleContainerTools/kpt/pkg/api/plan/v1alpha1"
+	"github.com/GoogleContainerTools/kpt/thirdparty/cli-utils/apply"
+	"github.com/google/go-cmp/cmp"
+	"github.com/stretchr/testify/require"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"sigs.k8s.io/cli-utils/pkg/apply/event"
+	"sigs.k8s.io/cli-utils/pkg/inventory"
+	"sigs.k8s.io/cli-utils/pkg/object"
+	"sigs.k8s.io/cli-utils/pkg/testutil"
+)
+
+var (
+	deploymentYAML = `
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: foo
+  namespace: default
+spec:
+  replicas: 1
+`
+)
+
+func TestClusterPlanner(t *testing.T) {
+	testCases := map[string]struct {
+		resources        []*unstructured.Unstructured
+		clusterResources []*unstructured.Unstructured
+		events           []event.Event
+
+		expectedPlan *planv1alpha1.Plan
+	}{
+		"single new resource": {
+			resources: []*unstructured.Unstructured{
+				testutil.Unstructured(t, deploymentYAML),
+			},
+			clusterResources: []*unstructured.Unstructured{},
+			events: []event.Event{
+				{
+					Type: event.InitType,
+					InitEvent: event.InitEvent{
+						ActionGroups: event.ActionGroupList{
+							{
+								Action: event.ApplyAction,
+								Name:   "apply-1",
+								Identifiers: []object.ObjMetadata{
+									testutil.ToIdentifier(t, deploymentYAML),
+								},
+							},
+						},
+					},
+				},
+				{
+					Type: event.ApplyType,
+					ApplyEvent: event.ApplyEvent{
+						GroupName:  "apply-1",
+						Identifier: testutil.ToIdentifier(t, deploymentYAML),
+						Operation:  event.ServersideApplied,
+						Resource:   testutil.Unstructured(t, deploymentYAML),
+					},
+				},
+			},
+			expectedPlan: &planv1alpha1.Plan{
+				ResourceMeta: planv1alpha1.ResourceMeta,
+				Spec: planv1alpha1.PlanSpec{
+					Actions: []planv1alpha1.Action{
+						{
+							Type:      planv1alpha1.Create,
+							Name:      "foo",
+							Namespace: "default",
+							Group:     "apps",
+							Kind:      "Deployment",
+							After:     testutil.Unstructured(t, deploymentYAML),
+						},
+					},
+				},
+			},
+		},
+	}
+
+	for tn := range testCases {
+		tc := testCases[tn]
+		t.Run(tn, func(t *testing.T) {
+			ctx := context.Background()
+
+			applier := &FakeApplier{
+				events: tc.events,
+			}
+
+			fakeResourceFetcher := &FakeResourceFetcher{
+				resources: tc.clusterResources,
+			}
+
+			plan, err := (&ClusterPlanner{
+				applier:         applier,
+				resourceFetcher: fakeResourceFetcher,
+			}).BuildPlan(ctx, &FakeInventoryInfo{}, []*unstructured.Unstructured{}, Options{})
+			require.NoError(t, err)
+
+			if diff := cmp.Diff(tc.expectedPlan, plan); diff != "" {
+				t.Errorf("plan mismatch (-want +got):\n%s", diff)
+			}
+		})
+	}
+}
+
+type FakeApplier struct {
+	events []event.Event
+}
+
+func (f *FakeApplier) Run(context.Context, inventory.Info, object.UnstructuredSet, apply.ApplierOptions) <-chan event.Event {
+	eventChannel := make(chan event.Event)
+	go func() {
+		defer close(eventChannel)
+		for i := range f.events {
+			eventChannel <- f.events[i]
+		}
+	}()
+	return eventChannel
+}
+
+type FakeResourceFetcher struct {
+	resources []*unstructured.Unstructured
+}
+
+func (frf *FakeResourceFetcher) FetchResource(_ context.Context, id object.ObjMetadata) (*unstructured.Unstructured, bool, error) {
+	for i := range frf.resources {
+		r := frf.resources[i]
+		rid := object.UnstructuredToObjMetadata(r)
+		if rid == id {
+			return r, true, nil
+		}
+	}
+	return nil, false, nil
+}
+
+type FakeInventoryInfo struct{}
+
+func (fii *FakeInventoryInfo) Namespace() string {
+	return ""
+}
+
+func (fii *FakeInventoryInfo) Name() string {
+	return ""
+}
+
+func (fii *FakeInventoryInfo) ID() string {
+	return ""
+}
+
+func (fii *FakeInventoryInfo) Strategy() inventory.Strategy {
+	return inventory.NameStrategy
+}
diff --git a/pkg/test/live/runner.go b/pkg/test/live/runner.go
index 26ddcf2c..ef0a8c1b 100644
--- a/pkg/test/live/runner.go
+++ b/pkg/test/live/runner.go
@@ -72,9 +72,8 @@ 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...)
-	t.Logf("Running command: kpt %s", strings.Join(args, " "))
-	cmd := exec.Command("kpt", args...)
+	t.Logf("Running command: kpt %s", strings.Join(r.Config.KptArgs, " "))
+	cmd := exec.Command("kpt", r.Config.KptArgs...)
 	cmd.Dir = filepath.Join(r.Path, "resources")
 
 	var outBuf bytes.Buffer
@@ -106,7 +105,11 @@ func (r *Runner) VerifyStderr(t *testing.T, stderr string) {
 }
 
 func prepOutput(t *testing.T, s string) string {
-	return strings.TrimSpace(substituteTimestamps(removeStatusEvents(t, s)))
+	txt := removeStatusEvents(t, s)
+	txt = substituteTimestamps(txt)
+	txt = substituteUIDs(txt)
+	txt = substituteResourceVersion(txt)
+	return strings.TrimSpace(txt)
 }
 
 func (r *Runner) VerifyInventory(t *testing.T, name, namespace string) {
@@ -179,6 +182,18 @@ func substituteTimestamps(text string) string {
 	return timestampRegexp.ReplaceAllString(text, "<TIMESTAMP>")
 }
 
+var uidRegexp = regexp.MustCompile(`[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`)
+
+func substituteUIDs(text string) string {
+	return uidRegexp.ReplaceAllLiteralString(text, "<UID>")
+}
+
+var resourceVersionRegexp = regexp.MustCompile(`resourceVersion: "[0-9]+"`)
+
+func substituteResourceVersion(text string) string {
+	return resourceVersionRegexp.ReplaceAllLiteralString(text, "resourceVersion: \"<RV>\"")
+}
+
 var statuses = []status.Status{
 	status.InProgressStatus,
 	status.CurrentStatus,