Repository: GoogleContainerTools/kpt
Pull request #3182 (2022-05-16 21:27:42)

Simplify Plan resource
Follow up on some of the comments from https://github.com/GoogleContainerTools/kpt/pull/3123:

* The property containing the resource from before the apply operation has been renamed from `before` to `original`, and the one containing the resource after the apply (i.e. the resource returned by the APIServer on a dry-run) is renamed from `after` to `updated`.
* The `original` and `updated` properties no longer contains a nested `object` property (as a result of marshaling an Unstructured object), but contains the resources directly underneath.
* The KRM output is generated directly with kyaml and we no longer provide an exported `Plan` type.



Patch:
diff --git a/internal/cmdplan/command.go b/internal/cmdplan/command.go
index cb8db7cc..2ec5b143 100644
--- a/internal/cmdplan/command.go
+++ b/internal/cmdplan/command.go
@@ -22,7 +22,6 @@ import (
 	"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"
@@ -154,34 +153,34 @@ func (r *Runner) RunE(c *cobra.Command, args []string) error {
 	return fmt.Errorf("unknown output format %s", r.output)
 }
 
-func printText(plan *v1alpha1.Plan, objs []*unstructured.Unstructured, ioStreams genericclioptions.IOStreams) error {
+func printText(plan *kptplanner.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]
+	for i := range plan.Actions {
+		action := plan.Actions[i]
 		switch action.Type {
-		case v1alpha1.Create:
+		case kptplanner.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:
+		case kptplanner.Unchanged:
 			// Do nothing.
-		case v1alpha1.Delete:
+		case kptplanner.Delete:
 			printEntryWithColor("-", print.RED, action, ioStreams)
-		case v1alpha1.Update:
+		case kptplanner.Update:
 			printEntry(" ", action, ioStreams)
-			findAndPrintDiff(action.Before, action.After, ContentPrefix, ioStreams)
-		case v1alpha1.Skip:
+			findAndPrintDiff(action.Original, action.Updated, ContentPrefix, ioStreams)
+		case kptplanner.Skip:
 			// TODO: provide more information about why the resource was skipped.
 			printEntryWithColor("=", print.YELLOW, action, ioStreams)
-		case v1alpha1.Error:
+		case kptplanner.Error:
 			printEntry("!", action, ioStreams)
 			printWithPrefix(action.Error, ContentPrefix, ioStreams)
 		}
@@ -190,21 +189,21 @@ func printText(plan *v1alpha1.Plan, objs []*unstructured.Unstructured, ioStreams
 	return nil
 }
 
-func hasChanges(plan *v1alpha1.Plan) bool {
-	for _, a := range plan.Spec.Actions {
-		if a.Type != v1alpha1.Unchanged {
+func hasChanges(plan *kptplanner.Plan) bool {
+	for _, a := range plan.Actions {
+		if a.Type != kptplanner.Unchanged {
 			return true
 		}
 	}
 	return false
 }
 
-func printEntryWithColor(prefix string, color print.Color, action v1alpha1.Action, ioStreams genericclioptions.IOStreams) {
+func printEntryWithColor(prefix string, color print.Color, action kptplanner.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) {
+func printEntry(prefix string, action kptplanner.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)
 }
 
@@ -265,24 +264,82 @@ func printWithPrefix(text, prefix string, ioStreams genericclioptions.IOStreams)
 
 // 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)
+func printKRM(
+	plan *kptplanner.Plan,
+	ioStreams genericclioptions.IOStreams,
+) error {
+	planResource, err := yaml.Parse(strings.TrimSpace(`
+apiVersion: kpt.dev/v1alpha1
+kind: Plan
+metadata:
+  name: plan
+  annotations:
+    config.kubernetes.io/local-config: true	
+`))
 	if err != nil {
-		return fmt.Errorf("failed to marshall plan into yaml: %w", err)
+		return fmt.Errorf("unable to create yaml document: %w", err)
 	}
-	rn, err := yaml.Parse(string(b))
+
+	sNode, err := planResource.Pipe(yaml.LookupCreate(yaml.SequenceNode, "spec", "actions"))
 	if err != nil {
-		return fmt.Errorf("failed to parse plan: %w", err)
+		return fmt.Errorf("unable to update yaml document: %w", err)
 	}
+
+	for i := range plan.Actions {
+		action := plan.Actions[i]
+		a := yaml.NewRNode(&yaml.Node{Kind: yaml.MappingNode})
+		fields := map[string]*yaml.RNode{
+			"action":     yaml.NewScalarRNode(string(action.Type)),
+			"apiVersion": yaml.NewScalarRNode(action.Group),
+			"kind":       yaml.NewScalarRNode(action.Kind),
+			"name":       yaml.NewScalarRNode(action.Name),
+			"namespace":  yaml.NewScalarRNode(action.Namespace),
+		}
+		if action.Original != nil {
+			r, err := unstructuredToRNode(action.Original)
+			if err != nil {
+				return err
+			}
+			fields["original"] = r
+		}
+		if action.Updated != nil {
+			r, err := unstructuredToRNode(action.Updated)
+			if err != nil {
+				return err
+			}
+			fields["updated"] = r
+		}
+		if action.Error != "" {
+			fields["error"] = yaml.NewScalarRNode(action.Error)
+		}
+
+		for key, val := range fields {
+			if err := a.PipeE(yaml.SetField(key, val)); err != nil {
+				return fmt.Errorf("unable to update yaml document: %w", err)
+			}
+		}
+		if err := sNode.PipeE(yaml.Append(a.YNode())); err != nil {
+			return fmt.Errorf("unable to update yaml document: %w", err)
+		}
+	}
+
 	writer := &kio.ByteWriter{
 		Writer:                ioStreams.Out,
 		KeepReaderAnnotations: true,
 		WrappingAPIVersion:    kio.ResourceListAPIVersion,
 		WrappingKind:          kio.ResourceListKind,
 	}
-	err = writer.Write([]*yaml.RNode{rn})
+	err = writer.Write([]*yaml.RNode{planResource})
 	if err != nil {
 		return fmt.Errorf("failed to write resources: %w", err)
 	}
 	return nil
 }
+
+func unstructuredToRNode(u *unstructured.Unstructured) (*yaml.RNode, error) {
+	b, err := yaml.Marshal(u.Object)
+	if err != nil {
+		return nil, err
+	}
+	return yaml.Parse((string(b)))
+}
diff --git a/pkg/api/plan/v1alpha1/types.go b/pkg/api/plan/v1alpha1/types.go
deleted file mode 100644
index bd5e17a4..00000000
--- a/pkg/api/plan/v1alpha1/types.go
+++ /dev/null
@@ -1,67 +0,0 @@
-// 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
index 2210e7ff..0c32d87d 100644
--- a/pkg/live/planner/cluster.go
+++ b/pkg/live/planner/cluster.go
@@ -19,7 +19,6 @@ import (
 	"fmt"
 	"reflect"
 
-	planv1alpha1 "github.com/GoogleContainerTools/kpt/pkg/api/plan/v1alpha1"
 	"github.com/GoogleContainerTools/kpt/pkg/live"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/api/meta"
@@ -34,6 +33,19 @@ import (
 	"sigs.k8s.io/cli-utils/pkg/object"
 )
 
+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
+}
+
 func NewClusterPlanner(f util.Factory) (*ClusterPlanner, error) {
 	fetcher, err := NewResourceFetcher(f)
 	if err != nil {
@@ -59,33 +71,43 @@ func NewClusterPlanner(f util.Factory) (*ClusterPlanner, error) {
 	}, nil
 }
 
-type Applier interface {
-	Run(ctx context.Context, invInfo inventory.Info, objects object.UnstructuredSet, options apply.ApplierOptions) <-chan event.Event
-}
+type ActionType string
 
-type ResourceFetcher interface {
-	FetchResource(ctx context.Context, id object.ObjMetadata) (*unstructured.Unstructured, bool, error)
+const (
+	Create    ActionType = "Create"
+	Unchanged ActionType = "Unchanged"
+	Delete    ActionType = "Delete"
+	Update    ActionType = "Update"
+	Skip      ActionType = "Skip"
+	Error     ActionType = "Error"
+)
+
+type Plan struct {
+	Actions []Action
 }
 
-type ClusterPlanner struct {
-	applier         Applier
-	resourceFetcher ResourceFetcher
+type Action struct {
+	Type      ActionType
+	Group     string
+	Kind      string
+	Name      string
+	Namespace string
+	Original  *unstructured.Unstructured
+	Updated   *unstructured.Unstructured
+	Error     string
 }
 
 type Options struct {
 	ServerSideOptions common.ServerSideOptions
 }
 
-func (r *ClusterPlanner) BuildPlan(ctx context.Context, inv inventory.Info, objects []*unstructured.Unstructured, o Options) (*planv1alpha1.Plan, error) {
+func (r *ClusterPlanner) BuildPlan(ctx context.Context, inv inventory.Info, objects []*unstructured.Unstructured, o Options) (*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,
-		},
+	return &Plan{
+		Actions: actions,
 	}, nil
 }
 
@@ -94,13 +116,13 @@ func (r *ClusterPlanner) dryRunForPlan(
 	inv inventory.Info,
 	objects []*unstructured.Unstructured,
 	o Options,
-) ([]planv1alpha1.Action, error) {
+) ([]Action, error) {
 	eventCh := r.applier.Run(ctx, inv, objects, apply.ApplierOptions{
 		DryRunStrategy:    common.DryRunServer,
 		ServerSideOptions: o.ServerSideOptions,
 	})
 
-	var actions []planv1alpha1.Action
+	var actions []Action
 	var err error
 	for e := range eventCh {
 		if e.Type == event.InitType {
@@ -149,70 +171,70 @@ func (r *ClusterPlanner) dryRunForPlan(
 	return actions, err
 }
 
-func handleApplyEvent(e event.Event, a planv1alpha1.Action) planv1alpha1.Action {
+func handleApplyEvent(e event.Event, a Action) Action {
 	if e.ApplyEvent.Error != nil {
-		a.Type = planv1alpha1.Error
+		a.Type = Error
 		a.Error = e.ApplyEvent.Error.Error()
 	} else {
 		switch e.ApplyEvent.Status {
 		case event.ApplySkipped:
-			a.Type = planv1alpha1.Skip
+			a.Type = Skip
 		case event.ApplySuccessful:
-			a.After = e.ApplyEvent.Resource
-			if a.Before != nil {
+			a.Updated = e.ApplyEvent.Resource
+			if a.Original != 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
+				if reflect.DeepEqual(a.Original, a.Updated) {
+					a.Type = Unchanged
 				} else {
-					a.Type = planv1alpha1.Update
+					a.Type = Update
 				}
 			} else {
-				a.Type = planv1alpha1.Create
+				a.Type = Create
 			}
 		}
 	}
 	return a
 }
 
-func handlePruneEvent(e event.Event, a planv1alpha1.Action) planv1alpha1.Action {
+func handlePruneEvent(e event.Event, a Action) Action {
 	if e.PruneEvent.Error != nil {
-		a.Type = planv1alpha1.Error
+		a.Type = Error
 		a.Error = e.PruneEvent.Error.Error()
 	} else {
 		switch e.PruneEvent.Status {
 		case event.PruneSuccessful:
-			a.Type = planv1alpha1.Delete
+			a.Type = 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
+			a.Type = Skip
 		}
 	}
 	return a
 }
 
-func handleDeleteEvent(e event.Event, a planv1alpha1.Action) planv1alpha1.Action {
+func handleDeleteEvent(e event.Event, a Action) Action {
 	if e.DeleteEvent.Error != nil {
-		a.Type = planv1alpha1.Error
+		a.Type = Error
 		a.Error = e.DeleteEvent.Error.Error()
 	} else {
 		switch e.DeleteEvent.Status {
 		case event.DeleteSuccessful:
-			a.Type = planv1alpha1.Delete
+			a.Type = Delete
 		case event.DeleteSkipped:
-			a.Type = planv1alpha1.Skip
+			a.Type = Skip
 		}
 	}
 	return a
 }
 
-func (r *ClusterPlanner) fetchResources(ctx context.Context, e event.Event) ([]planv1alpha1.Action, error) {
-	var actions []planv1alpha1.Action
+func (r *ClusterPlanner) fetchResources(ctx context.Context, e event.Event) ([]Action, error) {
+	var actions []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) {
@@ -224,12 +246,12 @@ func (r *ClusterPlanner) fetchResources(ctx context.Context, e event.Event) ([]p
 			if err != nil && !meta.IsNoMatchError(err) {
 				return nil, err
 			}
-			actions = append(actions, planv1alpha1.Action{
+			actions = append(actions, Action{
 				Group:     id.GroupKind.Group,
 				Kind:      id.GroupKind.Kind,
 				Name:      id.Name,
 				Namespace: id.Namespace,
-				Before:    u,
+				Original:  u,
 			})
 		}
 	}
@@ -279,7 +301,7 @@ func (rf *resourceFetcher) FetchResource(ctx context.Context, id object.ObjMetad
 	return u, true, nil
 }
 
-func indexForIdentifier(id object.ObjMetadata, actions []planv1alpha1.Action) int {
+func indexForIdentifier(id object.ObjMetadata, actions []Action) int {
 	for i := range actions {
 		a := actions[i]
 		if a.Group == id.GroupKind.Group &&
diff --git a/porch/func/internal/podevaluator.go b/porch/func/internal/podevaluator.go
index 2817d4be..a8d06d12 100644
--- a/porch/func/internal/podevaluator.go
+++ b/porch/func/internal/podevaluator.go
@@ -488,6 +488,9 @@ func (pm *podManager) retrieveOrCreatePod(ctx context.Context, image string, ttl
 			Namespace: pm.namespace,
 			Annotations: map[string]string{
 				reclaimAfterAnnotation: fmt.Sprintf("%v", time.Now().Add(ttl).Unix()),
+				// Add the following annotation to make it work well with the cluster autoscaler.
+				// https://github.com/kubernetes/autoscaler/blob/master/cluster-autoscaler/FAQ.md#what-types-of-pods-can-prevent-ca-from-removing-a-node
+				"cluster-autoscaler.kubernetes.io/safe-to-evict": "true",
 			},
 			// The function runner can use the label to retrieve the pod. Label is function name + part of its digest.
 			// If a function has more than one tags pointing to the same digest, we can reuse the same pod.
diff --git a/site/book/01-getting-started/01-system-requirements.md b/site/book/01-getting-started/01-system-requirements.md
index 62c261eb..81b819a6 100644
--- a/site/book/01-getting-started/01-system-requirements.md
+++ b/site/book/01-getting-started/01-system-requirements.md
@@ -13,9 +13,26 @@ $ kpt version
 
 kpt requires that you have [Git] installed and configured.
 
-## Docker
+## Container Runtime
 
-kpt requires that you have [Docker] installed and configured.
+`kpt` requires you to have at least one of the following runtimes installed and configured.
+
+
+
+### Docker
+
+Please follow the [instructions][install-docker] to install and configure Docker.
+
+### Podman
+
+Please follow the [instructions][install-podman] to install and configure Podman.
+
+If you want to set up rootless container runtime, [this][rootless] may be a
+useful resource for you.
+
+Environment variable can be used to control which container runtime to use. More
+details can be found in the reference documents for [`kpt fn render`](/reference/cli/fn/render/)
+and [`kpt fn eval`](/reference/cli/fn/eval/).
 
 ## Kubernetes cluster
 
@@ -24,6 +41,8 @@ In order to deploy the examples, you need a Kubernetes cluster and a configured
 For testing purposes, [kind] tool is useful for running ephemeral Kubernetes cluster on your local host.
 
 [install]: /installation/
-[docker]: https://docs.docker.com/get-docker/
+[install-docker]: https://docs.docker.com/get-docker/
+[install-podman]: https://podman.io/getting-started/installation
+[rootless]: https://rootlesscontaine.rs/
 [git]: https://git-scm.com/book/en/v2/Getting-Started-Installing-Git
 [kind]: https://kind.sigs.k8s.io/docs/user/quick-start/


Test patch:
diff --git a/pkg/live/planner/cluster_test.go b/pkg/live/planner/cluster_test.go
index 3c98fbb4..c09a0ead 100644
--- a/pkg/live/planner/cluster_test.go
+++ b/pkg/live/planner/cluster_test.go
@@ -18,7 +18,6 @@ import (
 	"context"
 	"testing"
 
-	planv1alpha1 "github.com/GoogleContainerTools/kpt/pkg/api/plan/v1alpha1"
 	"github.com/google/go-cmp/cmp"
 	"github.com/stretchr/testify/require"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -47,7 +46,7 @@ func TestClusterPlanner(t *testing.T) {
 		clusterResources []*unstructured.Unstructured
 		events           []event.Event
 
-		expectedPlan *planv1alpha1.Plan
+		expectedPlan *Plan
 	}{
 		"single new resource": {
 			resources: []*unstructured.Unstructured{
@@ -79,18 +78,15 @@ func TestClusterPlanner(t *testing.T) {
 					},
 				},
 			},
-			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),
-						},
+			expectedPlan: &Plan{
+				Actions: []Action{
+					{
+						Type:      Create,
+						Name:      "foo",
+						Namespace: "default",
+						Group:     "apps",
+						Kind:      "Deployment",
+						Updated:   testutil.Unstructured(t, deploymentYAML),
 					},
 				},
 			},