Repository: GoogleContainerTools/kpt
Pull request #3433 (2022-08-03 13:07:03)

Include the Config Connector status reader in the status watcher
This fixes a regression where we didn't include the custom status reader for Config Connector resources when we switched from the StatusPoller to the StatusWatcher.

This also updates to use cli-utils from the master branch, so we need to make sure we have a release before the next kpt release.

We should also remove the remaining places where we still use the poller (the `live status` and `live install-resource-group` commands), but I want to do that in a separate PR


Patch:
diff --git a/go.mod b/go.mod
index 587cc497..46d23554 100644
--- a/go.mod
+++ b/go.mod
@@ -26,7 +26,7 @@ require (
 	k8s.io/component-base v0.24.0
 	k8s.io/klog/v2 v2.60.1
 	k8s.io/kubectl v0.24.0
-	sigs.k8s.io/cli-utils v0.32.0
+	sigs.k8s.io/cli-utils v0.32.1-0.20220804181346-99ba319afc0b
 	sigs.k8s.io/controller-runtime v0.11.0
 	sigs.k8s.io/kustomize/api v0.12.1
 	sigs.k8s.io/kustomize/kyaml v0.13.9
diff --git a/go.sum b/go.sum
index 34ea8a97..fb48d958 100644
--- a/go.sum
+++ b/go.sum
@@ -1192,8 +1192,8 @@ rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
 sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.25/go.mod h1:Mlj9PNLmG9bZ6BHFwFKDo5afkpWyUISkb9Me0GnK66I=
 sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.30/go.mod h1:fEO7lRTdivWO2qYVCVG7dEADOMo/MLDCVr8So2g88Uw=
-sigs.k8s.io/cli-utils v0.32.0 h1:776y8z6gbBFTxfJ78lkHK6tbzK+MZTH6+ycP1gh16aw=
-sigs.k8s.io/cli-utils v0.32.0/go.mod h1:g/zB9hJ5eUN7zIEBIxrO0CwhXU4YISJ+BkLJzvWwlEs=
+sigs.k8s.io/cli-utils v0.32.1-0.20220804181346-99ba319afc0b h1:Z3TNRZdGPgm0ZA1GUIDtDD0ItA9/jPfp6eceYWw/fvY=
+sigs.k8s.io/cli-utils v0.32.1-0.20220804181346-99ba319afc0b/go.mod h1:g/zB9hJ5eUN7zIEBIxrO0CwhXU4YISJ+BkLJzvWwlEs=
 sigs.k8s.io/controller-runtime v0.11.0 h1:DqO+c8mywcZLFJWILq4iktoECTyn30Bkj0CwgqMpZWQ=
 sigs.k8s.io/controller-runtime v0.11.0/go.mod h1:KKwLiTooNGu+JmLZGn9Sl3Gjmfj66eMbCQznLP5zcqA=
 sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs=
diff --git a/internal/cmdapply/cmdapply.go b/internal/cmdapply/cmdapply.go
index a3786abe..ec83f30e 100644
--- a/internal/cmdapply/cmdapply.go
+++ b/internal/cmdapply/cmdapply.go
@@ -26,6 +26,7 @@ import (
 	"github.com/GoogleContainerTools/kpt/internal/util/argutil"
 	"github.com/GoogleContainerTools/kpt/internal/util/strings"
 	"github.com/GoogleContainerTools/kpt/pkg/live"
+	"github.com/GoogleContainerTools/kpt/pkg/status"
 	"github.com/spf13/cobra"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -227,9 +228,15 @@ func runApply(r *Runner, invInfo inventory.Info, objs []*unstructured.Unstructur
 		return err
 	}
 
+	statusWatcher, err := status.NewStatusWatcher(r.factory)
+	if err != nil {
+		return err
+	}
+
 	applier, err := apply.NewApplierBuilder().
 		WithFactory(r.factory).
 		WithInventoryClient(invClient).
+		WithStatusWatcher(statusWatcher).
 		Build()
 	if err != nil {
 		return err
diff --git a/internal/cmddestroy/cmddestroy.go b/internal/cmddestroy/cmddestroy.go
index 6cddfd54..b47999ef 100644
--- a/internal/cmddestroy/cmddestroy.go
+++ b/internal/cmddestroy/cmddestroy.go
@@ -23,6 +23,7 @@ import (
 	"github.com/GoogleContainerTools/kpt/internal/util/argutil"
 	"github.com/GoogleContainerTools/kpt/internal/util/strings"
 	"github.com/GoogleContainerTools/kpt/pkg/live"
+	"github.com/GoogleContainerTools/kpt/pkg/status"
 	"github.com/spf13/cobra"
 	"k8s.io/cli-runtime/pkg/genericclioptions"
 	"k8s.io/kubectl/pkg/cmd/util"
@@ -163,7 +164,16 @@ func runDestroy(r *Runner, inv inventory.Info, dryRunStrategy common.DryRunStrat
 		return err
 	}
 
-	destroyer, err := apply.NewDestroyer(r.factory, invClient)
+	statusWatcher, err := status.NewStatusWatcher(r.factory)
+	if err != nil {
+		return err
+	}
+
+	destroyer, err := apply.NewDestroyerBuilder().
+		WithFactory(r.factory).
+		WithInventoryClient(invClient).
+		WithStatusWatcher(statusWatcher).
+		Build()
 	if err != nil {
 		return err
 	}
diff --git a/internal/cmdmigrate/migratecmd.go b/internal/cmdmigrate/migratecmd.go
index 26c89544..87d25381 100644
--- a/internal/cmdmigrate/migratecmd.go
+++ b/internal/cmdmigrate/migratecmd.go
@@ -189,16 +189,18 @@ func (mr *MigrateRunner) retrieveConfigMapInv(reader io.Reader, args []string) (
 	}
 	cmInvObj, _, err := inventory.SplitUnstructureds(objs)
 	if err != nil {
+		fmt.Fprintln(mr.ioStreams.Out, "no ConfigMap inventory...completed")
 		return nil, err
 	}
 
 	// cli-utils treats any resource that contains the inventory-id label as an inventory object. We should
 	// ignore any inventories that are stored as ResourceGroup resources since they do not need migration.
-	if cmInvObj == nil || cmInvObj.GetKind() == rgfilev1alpha1.RGFileKind {
+	if cmInvObj.GetKind() == rgfilev1alpha1.RGFileKind {
 		// No ConfigMap inventory means the migration has already run before.
 		fmt.Fprintln(mr.ioStreams.Out, "no ConfigMap inventory...completed")
 		return nil, &inventory.NoInventoryObjError{}
 	}
+
 	cmInv := inventory.WrapInventoryInfoObj(cmInvObj)
 	fmt.Fprintf(mr.ioStreams.Out, "success (inventory-id: %s)\n", cmInv.ID())
 	return cmInv, nil
diff --git a/internal/cmdrpkgupdate/command.go b/internal/cmdrpkgupdate/command.go
index e53a2cca..759d21ba 100644
--- a/internal/cmdrpkgupdate/command.go
+++ b/internal/cmdrpkgupdate/command.go
@@ -28,6 +28,9 @@ import (
 
 const (
 	command = "cmdrpkgupdate"
+
+	upstream   = "upstream"
+	downstream = "downstream"
 )
 
 func NewCommand(ctx context.Context, rcg *genericclioptions.ConfigFlags) *cobra.Command {
@@ -46,8 +49,10 @@ func newRunner(ctx context.Context, rcg *genericclioptions.ConfigFlags) *runner
 		Hidden:  porch.HidePorchCommands,
 	}
 	r.Command.Flags().StringVar(&r.revision, "revision", "", "Revision of the upstream package to update to.")
-	r.Command.Flags().BoolVar(&r.discover, "discover", false,
-		"If set, search for available updates instead of performing an update. If no arguments given, look at all package revisions.")
+	r.Command.Flags().StringVar(&r.discover, "discover", "",
+		`If set, search for available updates instead of performing an update. 
+Setting this to 'upstream' will discover upstream updates of downstream packages.
+Setting this to 'downstream' will discover downstream package revisions of upstream packages that need to be updated.`)
 	return r
 }
 
@@ -58,7 +63,7 @@ type runner struct {
 	Command *cobra.Command
 
 	revision string // Target package revision
-	discover bool   // If set, discover updates rather than do updates
+	discover string // If set, discover updates rather than do updates
 
 	// there are multiple places where we need access to all package revisions, so
 	// we store it in the runner
@@ -73,7 +78,7 @@ func (r *runner) preRunE(cmd *cobra.Command, args []string) error {
 	}
 	r.client = c
 
-	if !r.discover {
+	if r.discover == "" {
 		if len(args) < 1 {
 			return errors.E(op, fmt.Errorf("SOURCE_PACKAGE is a required positional argument"))
 		}
@@ -84,6 +89,8 @@ func (r *runner) preRunE(cmd *cobra.Command, args []string) error {
 		if r.revision == "" {
 			return errors.E(op, fmt.Errorf("revision is required"))
 		}
+	} else if r.discover != upstream && r.discover != downstream {
+		return errors.E(op, fmt.Errorf("argument for 'discover' must be one of 'upstream' or 'downstream'"))
 	}
 
 	packageRevisionList := porchapi.PackageRevisionList{}
@@ -98,18 +105,18 @@ func (r *runner) preRunE(cmd *cobra.Command, args []string) error {
 func (r *runner) runE(cmd *cobra.Command, args []string) error {
 	const op errors.Op = command + ".runE"
 
-	var err error
-	if r.discover {
-		err = r.discoverUpdates(cmd, args)
-	} else {
+	if r.discover == "" {
 		pr := r.findPackageRevision(args[0])
 		if pr == nil {
 			return errors.E(op, fmt.Errorf("could not find package revision %s", args[0]))
 		}
-		err = r.doUpdate(pr)
-		fmt.Fprintf(cmd.OutOrStdout(), "%s updated\n", pr.Name)
-	}
-	if err != nil {
+		if err := r.doUpdate(pr); err != nil {
+			return errors.E(op, err)
+		}
+		if _, err := fmt.Fprintf(cmd.OutOrStdout(), "%s updated\n", pr.Name); err != nil {
+			return errors.E(op, err)
+		}
+	} else if err := r.discoverUpdates(cmd, args); err != nil {
 		return errors.E(op, err)
 	}
 	return nil
diff --git a/internal/cmdrpkgupdate/discover.go b/internal/cmdrpkgupdate/discover.go
index 33fdb05e..95055415 100644
--- a/internal/cmdrpkgupdate/discover.go
+++ b/internal/cmdrpkgupdate/discover.go
@@ -16,6 +16,7 @@ package cmdrpkgupdate
 
 import (
 	"fmt"
+	"io"
 	"strings"
 
 	porchapi "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1"
@@ -29,7 +30,7 @@ import (
 func (r *runner) discoverUpdates(cmd *cobra.Command, args []string) error {
 	var prs []porchapi.PackageRevision
 	var errs []string
-	if len(args) == 0 {
+	if len(args) == 0 || r.discover == downstream {
 		prs = r.prs
 	} else {
 		for i := range args {
@@ -50,31 +51,50 @@ func (r *runner) discoverUpdates(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	var updates [][]string
+	switch r.discover {
+	case upstream:
+		return r.findUpstreamUpdates(prs, repositories, cmd.OutOrStdout())
+	case downstream:
+		return r.findDownstreamUpdates(prs, repositories, args, cmd.OutOrStdout())
+	default: // this should never happen, because we validate in preRunE
+		return fmt.Errorf("invalid argument %q for --discover", r.discover)
+	}
+}
+
+func (r *runner) findUpstreamUpdates(prs []porchapi.PackageRevision, repositories *configapi.RepositoryList, w io.Writer) error {
+	var upstreamUpdates [][]string
 	for _, pr := range prs {
 		availableUpdates, upstreamName := r.availableUpdates(pr.Status.UpstreamLock, repositories)
 		if len(availableUpdates) == 0 {
-			updates = append(updates, []string{pr.Name, upstreamName, "No update available"})
+			upstreamUpdates = append(upstreamUpdates, []string{pr.Name, upstreamName, "No update available"})
 		} else {
-			updates = append(updates, []string{pr.Name, upstreamName, strings.Join(availableUpdates, ", ")})
+			var revisions []string
+			for i := range availableUpdates {
+				revisions = append(revisions, availableUpdates[i].Spec.Revision)
+			}
+			upstreamUpdates = append(upstreamUpdates, []string{pr.Name, upstreamName, strings.Join(revisions, ", ")})
 		}
 	}
+	return printUpstreamUpdates(upstreamUpdates, w)
+}
 
-	w := printers.GetNewTabWriter(cmd.OutOrStdout())
-	if _, err := fmt.Fprintln(w, "PACKAGE REVISION\tUPSTREAM REPOSITORY\tUPSTREAM UPDATES"); err != nil {
-		return err
-	}
-	for _, pkgRev := range updates {
-		if _, err := fmt.Fprintln(w, strings.Join(pkgRev, "\t")); err != nil {
-			return err
+func (r *runner) findDownstreamUpdates(prs []porchapi.PackageRevision, repositories *configapi.RepositoryList,
+	args []string, w io.Writer) error {
+	// map from the upstream package revision to a list of its downstream package revisions
+	downstreamUpdatesMap := make(map[string][]porchapi.PackageRevision)
+
+	for _, pr := range prs {
+		availableUpdates, _ := r.availableUpdates(pr.Status.UpstreamLock, repositories)
+		for _, update := range availableUpdates {
+			key := fmt.Sprintf("%s:%s", update.Name, update.Spec.Revision)
+			downstreamUpdatesMap[key] = append(downstreamUpdatesMap[key], pr)
 		}
 	}
-
-	return w.Flush()
+	return printDownstreamUpdates(downstreamUpdatesMap, args, w)
 }
 
-func (r *runner) availableUpdates(upstreamLock *porchapi.UpstreamLock, repositories *configapi.RepositoryList) ([]string, string) {
-	var availableUpdates []string
+func (r *runner) availableUpdates(upstreamLock *porchapi.UpstreamLock, repositories *configapi.RepositoryList) ([]porchapi.PackageRevision, string) {
+	var availableUpdates []porchapi.PackageRevision
 	var upstream string
 
 	if upstreamLock == nil || upstreamLock.Git == nil {
@@ -96,7 +116,7 @@ func (r *runner) availableUpdates(upstreamLock *porchapi.UpstreamLock, repositor
 	}
 
 	// find a repo that matches the upstreamLock
-	var revisions []string
+	var revisions []porchapi.PackageRevision
 	for _, repo := range repositories.Items {
 		if repo.Spec.Type != configapi.RepositoryTypeGit {
 			// we are not currently supporting non-git repos for updates
@@ -112,7 +132,7 @@ func (r *runner) availableUpdates(upstreamLock *porchapi.UpstreamLock, repositor
 	}
 
 	for _, upstreamRevision := range revisions {
-		switch cmp := semver.Compare(upstreamRevision, currentUpstreamRevision); {
+		switch cmp := semver.Compare(upstreamRevision.Spec.Revision, currentUpstreamRevision); {
 		case cmp > 0: // upstreamRevision > currentUpstreamRevision
 			availableUpdates = append(availableUpdates, upstreamRevision)
 		case cmp == 0, cmp < 0: // upstreamRevision <= currentUpstreamRevision, do nothing
@@ -130,18 +150,80 @@ func (r *runner) getRepositories() (*configapi.RepositoryList, error) {
 }
 
 // fetches all package revision numbers for packages with the name upstreamPackageName from the repo
-func (r *runner) getUpstreamRevisions(repo configapi.Repository, upstreamPackageName string) []string {
-	var result []string
+func (r *runner) getUpstreamRevisions(repo configapi.Repository, upstreamPackageName string) []porchapi.PackageRevision {
+	var result []porchapi.PackageRevision
 	for _, pkgRev := range r.prs {
 		if pkgRev.Spec.Lifecycle != porchapi.PackageRevisionLifecyclePublished {
 			// only consider published packages
 			continue
 		}
-		if pkgRev.Spec.RepositoryName != repo.Name ||
-			pkgRev.Spec.PackageName != upstreamPackageName {
-			continue
+		if pkgRev.Spec.RepositoryName == repo.Name && pkgRev.Spec.PackageName == upstreamPackageName {
+			result = append(result, pkgRev)
 		}
-		result = append(result, pkgRev.Spec.Revision)
 	}
 	return result
 }
+
+func printUpstreamUpdates(upstreamUpdates [][]string, w io.Writer) error {
+	printer := printers.GetNewTabWriter(w)
+	if _, err := fmt.Fprintln(printer, "PACKAGE REVISION\tUPSTREAM REPOSITORY\tUPSTREAM UPDATES"); err != nil {
+		return err
+	}
+	for _, pkgRev := range upstreamUpdates {
+		if _, err := fmt.Fprintln(printer, strings.Join(pkgRev, "\t")); err != nil {
+			return err
+		}
+	}
+	return printer.Flush()
+}
+
+func printDownstreamUpdates(downstreamUpdatesMap map[string][]porchapi.PackageRevision, args []string, w io.Writer) error {
+	var downstreamUpdates [][]string
+	for upstreamPkgRev, downstreamPkgRevs := range downstreamUpdatesMap {
+		split := strings.Split(upstreamPkgRev, ":")
+		upstreamPkgRevName := split[0]
+		upstreamPkgRevNum := split[1]
+		for _, downstreamPkgRev := range downstreamPkgRevs {
+			// figure out which upstream revision the downstream revision is based on
+			lastIndex := strings.LastIndex(downstreamPkgRev.Status.UpstreamLock.Git.Ref, "v")
+			if lastIndex < 0 {
+				// this ref isn't formatted the way that porch expects
+				continue
+			}
+			downstreamRev := downstreamPkgRev.Status.UpstreamLock.Git.Ref[lastIndex:]
+			downstreamUpdates = append(downstreamUpdates,
+				[]string{upstreamPkgRevName, downstreamPkgRev.Name, fmt.Sprintf("%s->%s", downstreamRev, upstreamPkgRevNum)})
+		}
+	}
+
+	var pkgRevsToPrint [][]string
+	if len(args) != 0 {
+		for _, arg := range args {
+			for _, pkgRev := range downstreamUpdates {
+				// filter out irrelevant packages based on provided args
+				if arg == pkgRev[0] {
+					pkgRevsToPrint = append(pkgRevsToPrint, pkgRev)
+				}
+			}
+		}
+	} else {
+		pkgRevsToPrint = downstreamUpdates
+	}
+
+	printer := printers.GetNewTabWriter(w)
+	if len(pkgRevsToPrint) == 0 {
+		if _, err := fmt.Fprintln(printer, "All downstream packages are up to date."); err != nil {
+			return err
+		}
+	} else {
+		if _, err := fmt.Fprintln(printer, "PACKAGE REVISION\tDOWNSTREAM PACKAGE\tDOWNSTREAM UPDATE"); err != nil {
+			return err
+		}
+		for _, pkgRev := range pkgRevsToPrint {
+			if _, err := fmt.Fprintln(printer, strings.Join(pkgRev, "\t")); err != nil {
+				return err
+			}
+		}
+	}
+	return printer.Flush()
+}
diff --git a/pkg/live/planner/cluster.go b/pkg/live/planner/cluster.go
index 0c32d87d..d66ea6f3 100644
--- a/pkg/live/planner/cluster.go
+++ b/pkg/live/planner/cluster.go
@@ -20,6 +20,7 @@ import (
 	"reflect"
 
 	"github.com/GoogleContainerTools/kpt/pkg/live"
+	"github.com/GoogleContainerTools/kpt/pkg/status"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/api/meta"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -57,9 +58,15 @@ func NewClusterPlanner(f util.Factory) (*ClusterPlanner, error) {
 		return nil, err
 	}
 
+	statusWatcher, err := status.NewStatusWatcher(f)
+	if err != nil {
+		return nil, err
+	}
+
 	applier, err := apply.NewApplierBuilder().
 		WithFactory(f).
 		WithInventoryClient(invClient).
+		WithStatusWatcher(statusWatcher).
 		Build()
 	if err != nil {
 		return nil, err
diff --git a/pkg/status/configconnector.go b/pkg/status/configconnector.go
index e2845da2..d313a37a 100644
--- a/pkg/status/configconnector.go
+++ b/pkg/status/configconnector.go
@@ -40,6 +40,12 @@ type ConfigConnectorStatusReader struct {
 	Mapper meta.RESTMapper
 }
 
+func NewConfigConnectorStatusReader(mapper meta.RESTMapper) engine.StatusReader {
+	return &ConfigConnectorStatusReader{
+		Mapper: mapper,
+	}
+}
+
 var _ engine.StatusReader = &ConfigConnectorStatusReader{}
 
 // Supports returns true for all Config Connector resources.
diff --git a/pkg/status/poller.go b/pkg/status/poller.go
index afe99339..529d805b 100644
--- a/pkg/status/poller.go
+++ b/pkg/status/poller.go
@@ -15,9 +15,14 @@
 package status
 
 import (
+	"time"
+
 	"k8s.io/kubectl/pkg/cmd/util"
 	"sigs.k8s.io/cli-utils/pkg/kstatus/polling"
+	"sigs.k8s.io/cli-utils/pkg/kstatus/polling/clusterreader"
 	"sigs.k8s.io/cli-utils/pkg/kstatus/polling/engine"
+	"sigs.k8s.io/cli-utils/pkg/kstatus/polling/statusreaders"
+	"sigs.k8s.io/cli-utils/pkg/kstatus/watcher"
 )
 
 func NewStatusPoller(f util.Factory) (*polling.StatusPoller, error) {
@@ -34,3 +39,26 @@ func NewStatusPoller(f util.Factory) (*polling.StatusPoller, error) {
 		},
 	})
 }
+
+func NewStatusWatcher(f util.Factory) (watcher.StatusWatcher, error) {
+	mapper, err := f.ToRESTMapper()
+	if err != nil {
+		return nil, err
+	}
+
+	dynamicClient, err := f.DynamicClient()
+	if err != nil {
+		return nil, err
+	}
+
+	return &watcher.DefaultStatusWatcher{
+		DynamicClient: dynamicClient,
+		Mapper:        mapper,
+		ResyncPeriod:  1 * time.Hour,
+		StatusReader:  statusreaders.NewStatusReader(mapper, NewConfigConnectorStatusReader(mapper)),
+		ClusterReader: &clusterreader.DynamicClusterReader{
+			DynamicClient: dynamicClient,
+			Mapper:        mapper,
+		},
+	}, nil
+}
diff --git a/porch/go.mod b/porch/go.mod
index 39b8f7ad..3c270b0c 100644
--- a/porch/go.mod
+++ b/porch/go.mod
@@ -181,7 +181,7 @@ require (
 	k8s.io/kube-openapi v0.0.0-20220413171646-5e7f5fdc6da6 // indirect
 	k8s.io/kubectl v0.24.0 // indirect
 	sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.30 // indirect
-	sigs.k8s.io/cli-utils v0.32.0 // indirect
+	sigs.k8s.io/cli-utils v0.32.1-0.20220804181346-99ba319afc0b // indirect
 	sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect
 	sigs.k8s.io/kustomize/api v0.12.1 // indirect
 	sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect
diff --git a/porch/go.sum b/porch/go.sum
index ddb0da0f..fd8c26fb 100644
--- a/porch/go.sum
+++ b/porch/go.sum
@@ -1828,8 +1828,8 @@ sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyz
 sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.25/go.mod h1:Mlj9PNLmG9bZ6BHFwFKDo5afkpWyUISkb9Me0GnK66I=
 sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.30 h1:dUk62HQ3ZFhD48Qr8MIXCiKA8wInBQCtuE4QGfFW7yA=
 sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.30/go.mod h1:fEO7lRTdivWO2qYVCVG7dEADOMo/MLDCVr8So2g88Uw=
-sigs.k8s.io/cli-utils v0.32.0 h1:776y8z6gbBFTxfJ78lkHK6tbzK+MZTH6+ycP1gh16aw=
-sigs.k8s.io/cli-utils v0.32.0/go.mod h1:g/zB9hJ5eUN7zIEBIxrO0CwhXU4YISJ+BkLJzvWwlEs=
+sigs.k8s.io/cli-utils v0.32.1-0.20220804181346-99ba319afc0b h1:Z3TNRZdGPgm0ZA1GUIDtDD0ItA9/jPfp6eceYWw/fvY=
+sigs.k8s.io/cli-utils v0.32.1-0.20220804181346-99ba319afc0b/go.mod h1:g/zB9hJ5eUN7zIEBIxrO0CwhXU4YISJ+BkLJzvWwlEs=
 sigs.k8s.io/controller-runtime v0.11.0/go.mod h1:KKwLiTooNGu+JmLZGn9Sl3Gjmfj66eMbCQznLP5zcqA=
 sigs.k8s.io/controller-runtime v0.11.1 h1:7YIHT2QnHJArj/dk9aUkYhfqfK5cIxPOX5gPECfdZLU=
 sigs.k8s.io/controller-runtime v0.11.1/go.mod h1:KKwLiTooNGu+JmLZGn9Sl3Gjmfj66eMbCQznLP5zcqA=
diff --git a/site/faq/README.md b/site/faq/README.md
index cf837035..b4e54a8d 100755
--- a/site/faq/README.md
+++ b/site/faq/README.md
@@ -125,9 +125,9 @@ don't have to alias it. It is pronounced "kept".
 [package orchestration]:
   https://github.com/GoogleContainerTools/kpt/blob/main/docs/design-docs/07-package-orchestration.md
 [the kubernetes resource model]:
-  https://github.com/kubernetes/community/blob/master/contributors/design-proposals/architecture/resource-management.md
+  https://github.com/kubernetes/design-proposals-archive/blob/main/architecture/resource-management.md
 [declarative application management in kubernetes]:
-  https://github.com/kubernetes/community/blob/master/contributors/design-proposals/architecture/declarative-application-management.md
+  https://github.com/kubernetes/design-proposals-archive/blob/main/architecture/declarative-application-management.md
 [rationale]: https://kpt.dev/guides/rationale
 [functions]: /reference/cli/fn/eval/
 [using functions]: /book/04-using-functions/


Test patch:
diff --git a/e2e/testdata/porch/rpkg-update/config.yaml b/e2e/testdata/porch/rpkg-update/config.yaml
index aec7a52c..12487445 100644
--- a/e2e/testdata/porch/rpkg-update/config.yaml
+++ b/e2e/testdata/porch/rpkg-update/config.yaml
@@ -66,7 +66,7 @@ commands:
       - rpkg
       - update
       - --namespace=rpkg-update
-      - --discover
+      - --discover=upstream
       - git-542e4d9fd8ca2ee94edf43aadb3ec31dc2fbd3a0
     stdout: |
       PACKAGE REVISION                               UPSTREAM REPOSITORY   UPSTREAM UPDATES
@@ -76,12 +76,21 @@ commands:
       - rpkg
       - update
       - --namespace=rpkg-update
-      - --discover
+      - --discover=upstream
     stdout: |
       PACKAGE REVISION                               UPSTREAM REPOSITORY   UPSTREAM UPDATES
       git-f1fbc2b72ee70b2f34e64c2630031d0560e6cf48                         No update available
       git-894137a40c0792e88a9b844491a95a466b085ca1                         No update available
       git-542e4d9fd8ca2ee94edf43aadb3ec31dc2fbd3a0   git                   v2
+  - args:
+      - alpha
+      - rpkg
+      - update
+      - --namespace=rpkg-update
+      - --discover=downstream
+    stdout: |
+      PACKAGE REVISION                               DOWNSTREAM PACKAGE                             DOWNSTREAM UPDATE
+      git-894137a40c0792e88a9b844491a95a466b085ca1   git-542e4d9fd8ca2ee94edf43aadb3ec31dc2fbd3a0   v0->v2
   - args:
       - alpha
       - rpkg
@@ -95,7 +104,7 @@ commands:
       - rpkg
       - update
       - --namespace=rpkg-update
-      - --discover
+      - --discover=upstream
     stdout: |
       PACKAGE REVISION                               UPSTREAM REPOSITORY   UPSTREAM UPDATES
       git-f1fbc2b72ee70b2f34e64c2630031d0560e6cf48                         No update available