Repository: GoogleContainerTools/kpt
Pull request #3614 (2022-10-12 02:19:55)

Package conditions
This adds support for readiness gates and conditions to kpt packages through new fields in the Kptfile, and exposes those fields in Porch through the `PackageRevision` resource.

The two most important aspects about this PR is:
* Is the fields we add to the Kptfile the correct ones? The patterns mirrors the `podReadinessGates` pretty closely, so it aligns well with prior art in Kubernetes.
* Should we expose fields in the Kptfile through the Porch API? We already expose the `UpstreamLock` read-only through the status object, but this allows for updates too by creating patches for the changes to the Kptfile. So in the git history for the Kptfile, it shows up identical to what it looks like if someone updates the Kptfile through `PackageRevisionResources`. Allowing updates through the Porch API directly simplifies updates with controllers.

One aspect of the current implementation that is a bit surprising, is that kpt updates both the conditions and the readiness gates as non-associative lists. This doesn't seem correct, but unfortunately the kyaml library can't identify the merge key and therefore falls back to treating it as a non-associative list. We could handle this either by adding custom merge logic for these fields, or provide a schema that provide this information to kyaml.

justinsb: A few nits, but LGTM

Patch:
diff --git a/commands/live/migrate/migratecmd.go b/commands/live/migrate/migratecmd.go
index e22d6202..eb50d13f 100644
--- a/commands/live/migrate/migratecmd.go
+++ b/commands/live/migrate/migratecmd.go
@@ -205,7 +205,7 @@ func (mr *Runner) retrieveConfigMapInv(reader io.Reader, args []string) (invento
 
 	// 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.GetKind() == rgfilev1alpha1.RGFileKind {
+	if cmInvObj.GetKind() == rgfilev1alpha1.ResourceGroupGVK().Kind {
 		// No ConfigMap inventory means the migration has already run before.
 		fmt.Fprintln(mr.ioStreams.Out, "no ConfigMap inventory...completed")
 		return nil, &inventory.NoInventoryObjError{}
diff --git a/internal/builtins/pkg_context.go b/internal/builtins/pkg_context.go
index 2638ee4f..614497ac 100644
--- a/internal/builtins/pkg_context.go
+++ b/internal/builtins/pkg_context.go
@@ -35,7 +35,7 @@ const (
 
 var (
 	configMapGVK = resid.NewGvk("", "v1", "ConfigMap")
-	kptfileGVK   = resid.NewGvk(kptfilev1.KptFileGroup, kptfilev1.KptFileVersion, kptfilev1.KptFileKind)
+	kptfileGVK   = resid.NewGvk(kptfilev1.KptFileGVK().Group, kptfilev1.KptFileGVK().Version, kptfilev1.KptFileGVK().Kind)
 )
 
 // PackageContextGenerator is a built-in KRM function that generates
diff --git a/internal/errors/resolver/pkg.go b/internal/errors/resolver/pkg.go
index f6338850..63e17a85 100644
--- a/internal/errors/resolver/pkg.go
+++ b/internal/errors/resolver/pkg.go
@@ -99,7 +99,7 @@ func resolveNestedErr(err error, tmplArgs map[string]interface{}) (ResolvedResul
 
 	var deprecatedv1alpha1KptfileError *pkg.DeprecatedKptfileError
 	if errors.As(err, &deprecatedv1alpha1KptfileError) &&
-		deprecatedv1alpha1KptfileError.Version == pkg.DeprecatedKptfileVersions[0] {
+		deprecatedv1alpha1KptfileError.Version == "v1alpha1" {
 		tmplArgs["version"] = deprecatedv1alpha1KptfileError.Version
 		errMsg := deprecatedv1Alpha1KptfileMsg
 		return ResolvedResult{
@@ -109,7 +109,7 @@ func resolveNestedErr(err error, tmplArgs map[string]interface{}) (ResolvedResul
 
 	var deprecatedv1alpha2KptfileError *pkg.DeprecatedKptfileError
 	if errors.As(err, &deprecatedv1alpha2KptfileError) &&
-		deprecatedv1alpha1KptfileError.Version == pkg.DeprecatedKptfileVersions[1] {
+		deprecatedv1alpha1KptfileError.Version == "v1alpha2" {
 		tmplArgs["version"] = deprecatedv1alpha2KptfileError.Version
 		errMsg := deprecatedv1Alpha2KptfileMsg
 		return ResolvedResult{
diff --git a/internal/pkg/pkg.go b/internal/pkg/pkg.go
index e4c55fa7..3e553fa7 100644
--- a/internal/pkg/pkg.go
+++ b/internal/pkg/pkg.go
@@ -30,7 +30,6 @@ import (
 	kptfilev1 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1"
 	rgfilev1alpha1 "github.com/GoogleContainerTools/kpt/pkg/api/resourcegroup/v1alpha1"
 	"k8s.io/apimachinery/pkg/runtime/schema"
-	"k8s.io/kubectl/pkg/util/slice"
 	"sigs.k8s.io/kustomize/kyaml/filesys"
 	"sigs.k8s.io/kustomize/kyaml/kio"
 	"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
@@ -45,17 +44,17 @@ const (
 	pkgPathAnnotation = "internal.config.kubernetes.io/package-path"
 )
 
-var DeprecatedKptfileVersions = []string{
-	"v1alpha1",
-	"v1alpha2",
+var DeprecatedKptfileVersions = []schema.GroupVersionKind{
+	kptfilev1.KptFileGVK().GroupKind().WithVersion("v1alpha1"),
+	kptfilev1.KptFileGVK().GroupKind().WithVersion("v1alpha2"),
 }
 
 // MatchAllKRM represents set of glob pattern to match all KRM
 // resources including Kptfile.
 var MatchAllKRM = append([]string{kptfilev1.KptFileName}, kio.MatchAll...)
 
-var SupportedKptfileVersions = []string{
-	kptfilev1.KptFileVersion,
+var SupportedKptfileVersions = []schema.GroupVersionKind{
+	kptfilev1.KptFileGVK(),
 }
 
 // KptfileError records errors regarding reading or parsing of a Kptfile.
@@ -286,18 +285,15 @@ func CheckKptfileVersion(content []byte) error {
 	if err != nil {
 		return err
 	}
+	gvk := gv.WithKind(kind)
 
 	switch {
 	// If the resource type matches what we are looking for, just return nil.
-	case gv.Group == kptfilev1.KptFileGroup &&
-		kind == kptfilev1.KptFileKind &&
-		isSupportedKptfileVersion(gv.Version):
+	case isSupportedKptfileVersion(gvk):
 		return nil
 	// If the kind and group is correct and the version is a known deprecated
 	// schema for the Kptfile, return DeprecatedKptfileError.
-	case gv.Group == kptfilev1.KptFileGroup &&
-		kind == kptfilev1.KptFileKind &&
-		isDeprecatedKptfileVersion(gv.Version):
+	case isDeprecatedKptfileVersion(gvk):
 		return &DeprecatedKptfileError{
 			Version: gv.Version,
 		}
@@ -310,12 +306,22 @@ func CheckKptfileVersion(content []byte) error {
 	}
 }
 
-func isDeprecatedKptfileVersion(version string) bool {
-	return slice.ContainsString(DeprecatedKptfileVersions, version, nil)
+func isDeprecatedKptfileVersion(gvk schema.GroupVersionKind) bool {
+	for _, v := range DeprecatedKptfileVersions {
+		if v == gvk {
+			return true
+		}
+	}
+	return false
 }
 
-func isSupportedKptfileVersion(version string) bool {
-	return slice.ContainsString(SupportedKptfileVersions, version, nil)
+func isSupportedKptfileVersion(gvk schema.GroupVersionKind) bool {
+	for _, v := range SupportedKptfileVersions {
+		if v == gvk {
+			return true
+		}
+	}
+	return false
 }
 
 // Pipeline returns the Pipeline section of the pkg's Kptfile.
@@ -777,7 +783,7 @@ func filterResourceGroups(input []*yaml.RNode) (output []*yaml.RNode, err error)
 			return nil, fmt.Errorf("failed to read metadata for resource %w", err)
 		}
 		// Filter out any non-ResourceGroup files.
-		if !(meta.APIVersion == rgfilev1alpha1.RGFileAPIVersion && meta.Kind == rgfilev1alpha1.RGFileKind) {
+		if !(meta.APIVersion == rgfilev1alpha1.ResourceGroupGVK().GroupVersion().String() && meta.Kind == rgfilev1alpha1.ResourceGroupGVK().Kind) {
 			continue
 		}
 
diff --git a/pkg/api/fnresult/v1/types.go b/pkg/api/fnresult/v1/types.go
index e6dc7848..29783fee 100644
--- a/pkg/api/fnresult/v1/types.go
+++ b/pkg/api/fnresult/v1/types.go
@@ -15,7 +15,7 @@
 package v1
 
 import (
-	kptfilev1 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1"
+	"k8s.io/apimachinery/pkg/runtime/schema"
 	"sigs.k8s.io/kustomize/kyaml/fn/framework"
 	"sigs.k8s.io/kustomize/kyaml/yaml"
 )
@@ -42,12 +42,25 @@ type Result struct {
 }
 
 const (
-	ResultListKind       = "FunctionResultList"
-	ResultListGroup      = kptfilev1.KptFileGroup
-	ResultListVersion    = kptfilev1.KptFileVersion
+	// Deprecated: prefer ResultListGVK
+	ResultListKind = "FunctionResultList"
+	// Deprecated: prefer ResultListGVK
+	ResultListGroup = "kpt.dev"
+	// Deprecated: prefer ResultListGVK
+	ResultListVersion = "v1"
+	// Deprecated: prefer ResultListGVK
 	ResultListAPIVersion = ResultListGroup + "/" + ResultListVersion
 )
 
+// KptFileGVK is the GroupVersionKind of FunctionResultList objects
+func ResultListGVK() schema.GroupVersionKind {
+	return schema.GroupVersionKind{
+		Group:   "kpt.dev",
+		Version: "v1",
+		Kind:    "FunctionResultList",
+	}
+}
+
 // ResultList contains aggregated results from multiple functions
 type ResultList struct {
 	yaml.ResourceMeta `yaml:",inline"`
diff --git a/pkg/api/kptfile/v1/types.go b/pkg/api/kptfile/v1/types.go
index c5bd8883..464a3b4d 100644
--- a/pkg/api/kptfile/v1/types.go
+++ b/pkg/api/kptfile/v1/types.go
@@ -20,17 +20,35 @@ package v1
 import (
 	"fmt"
 
+	"k8s.io/apimachinery/pkg/runtime/schema"
 	"sigs.k8s.io/kustomize/kyaml/yaml"
 )
 
 const (
-	KptFileName       = "Kptfile"
-	KptFileKind       = "Kptfile"
-	KptFileGroup      = "kpt.dev"
-	KptFileVersion    = "v1"
+	KptFileName = "Kptfile"
+
+	// Deprecated: prefer KptFileGVK
+	KptFileKind = "Kptfile"
+
+	// Deprecated: prefer KptFileGVK
+	KptFileGroup = "kpt.dev"
+
+	// Deprecated: prefer KptFileGVK
+	KptFileVersion = "v1"
+
+	// Deprecated: prefer KptFileGVK
 	KptFileAPIVersion = KptFileGroup + "/" + KptFileVersion
 )
 
+// KptFileGVK is the GroupVersionKind of Kptfile objects
+func KptFileGVK() schema.GroupVersionKind {
+	return schema.GroupVersionKind{
+		Group:   "kpt.dev",
+		Version: "v1",
+		Kind:    "Kptfile",
+	}
+}
+
 // TypeMeta is the TypeMeta for KptFile instances.
 var TypeMeta = yaml.ResourceMeta{
 	TypeMeta: yaml.TypeMeta{
@@ -57,6 +75,8 @@ type KptFile struct {
 
 	// Inventory contains parameters for the inventory object used in apply.
 	Inventory *Inventory `yaml:"inventory,omitempty" json:"inventory,omitempty"`
+
+	Status *Status `yaml:"status,omitempty" json:"status,omitempty"`
 }
 
 // OriginType defines the type of origin for a package.
@@ -192,6 +212,12 @@ type PackageInfo struct {
 
 	// Man is the path to documentation about the package
 	Man string `yaml:"man,omitempty" json:"man,omitempty"`
+
+	ReadinessGates []ReadinessGate `yaml:"readinessGates,omitempty" json:"readinessGates,omitempty"`
+}
+
+type ReadinessGate struct {
+	ConditionType string `yaml:"conditionType" json:"conditionType"`
 }
 
 // Subpackages declares a local or remote subpackage.
@@ -341,3 +367,25 @@ func (i Inventory) IsValid() bool {
 	// Name, Namespace InventoryID are required inventory fields, so we check these 3 fields.
 	return i.Name != "" && i.Namespace != "" && i.InventoryID != ""
 }
+
+type Status struct {
+	Conditions []Condition `yaml:"conditions,omitempty" json:"conditions,omitempty"`
+}
+
+type Condition struct {
+	Type string `yaml:"type" json:"type"`
+
+	Status ConditionStatus `yaml:"status" json:"status"`
+
+	Reason string `yaml:"reason,omitempty" json:"reason,omitempty"`
+
+	Message string `yaml:"message,omitempty" json:"message,omitempty"`
+}
+
+type ConditionStatus string
+
+const (
+	ConditionTrue    ConditionStatus = "True"
+	ConditionFalse   ConditionStatus = "False"
+	ConditionUnknown ConditionStatus = "Unknown"
+)
diff --git a/pkg/api/resourcegroup/v1alpha1/types.go b/pkg/api/resourcegroup/v1alpha1/types.go
index 0d9741ff..5ad6e458 100644
--- a/pkg/api/resourcegroup/v1alpha1/types.go
+++ b/pkg/api/resourcegroup/v1alpha1/types.go
@@ -18,20 +18,35 @@
 package v1alpha1
 
 import (
+	"k8s.io/apimachinery/pkg/runtime/schema"
 	"sigs.k8s.io/cli-utils/pkg/common"
 	"sigs.k8s.io/kustomize/kyaml/yaml"
 )
 
 const (
-	RGFileName       = "resourcegroup.yaml"
-	RGFileKind       = "ResourceGroup"
-	RGFileGroup      = "kpt.dev"
-	RGFileVersion    = "v1alpha1"
-	RGFileAPIVersion = RGFileGroup + "/" + RGFileVersion
+	RGFileName = "resourcegroup.yaml"
 	// RGInventoryIDLabel is the label name used for storing an inventory ID.
 	RGInventoryIDLabel = common.InventoryLabel
+
+	// Deprecated: prefer ResourceGroupGVK
+	RGFileKind = "ResourceGroup"
+	// Deprecated: prefer ResourceGroupGVK
+	RGFileGroup = "kpt.dev"
+	// Deprecated: prefer ResourceGroupGVK
+	RGFileVersion = "v1alpha1"
+	// Deprecated: prefer ResourceGroupGVK
+	RGFileAPIVersion = RGFileGroup + "/" + RGFileVersion
 )
 
+// ResourceGroupGVK is the GroupVersionKind of ResourceGroup objects
+func ResourceGroupGVK() schema.GroupVersionKind {
+	return schema.GroupVersionKind{
+		Group:   "kpt.dev",
+		Version: "v1alpha1",
+		Kind:    "ResourceGroup",
+	}
+}
+
 // DefaultMeta is the ResourceMeta for ResourceGroup instances.
 var DefaultMeta = yaml.ResourceMeta{
 	TypeMeta: yaml.TypeMeta{
diff --git a/pkg/kptfile/kptfileutil/util.go b/pkg/kptfile/kptfileutil/util.go
index b68f2964..a6fb9efa 100644
--- a/pkg/kptfile/kptfileutil/util.go
+++ b/pkg/kptfile/kptfileutil/util.go
@@ -271,6 +271,7 @@ func merge(localKf, updatedKf, originalKf *kptfilev1.KptFile) error {
 	localKf.Info = mergedKf.Info
 	localKf.Pipeline = mergedKf.Pipeline
 	localKf.Inventory = mergedKf.Inventory
+	localKf.Status = mergedKf.Status
 	return nil
 }
 
diff --git a/pkg/live/load.go b/pkg/live/load.go
index 424ffab7..b5b9c29c 100644
--- a/pkg/live/load.go
+++ b/pkg/live/load.go
@@ -26,6 +26,8 @@ import (
 	kptfilev1 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1"
 	rgfilev1alpha1 "github.com/GoogleContainerTools/kpt/pkg/api/resourcegroup/v1alpha1"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/klog/v2"
 	"k8s.io/kubectl/pkg/cmd/util"
 	"sigs.k8s.io/cli-utils/pkg/common"
 	"sigs.k8s.io/cli-utils/pkg/inventory"
@@ -195,8 +197,7 @@ type InventoryFilter struct {
 }
 
 func (i *InventoryFilter) Filter(object *yaml.RNode) (*yaml.RNode, error) {
-	if object.GetApiVersion() != kptfilev1.KptFileAPIVersion ||
-		object.GetKind() != kptfilev1.KptFileKind {
+	if GroupVersionKindForObject(object) != kptfilev1.KptFileGVK() {
 		return object, nil
 	}
 
@@ -220,9 +221,18 @@ type RGFilter struct {
 	Inventories []*rgfilev1alpha1.ResourceGroup
 }
 
+// GroupVersionKindForObject extracts the group/version/kind from an RNode holding a kubernetes object.
+func GroupVersionKindForObject(object *yaml.RNode) schema.GroupVersionKind {
+	apiVersion := object.GetApiVersion()
+	gv, err := schema.ParseGroupVersion(apiVersion)
+	if err != nil {
+		klog.Warningf("error parsing apiVersion=%q", apiVersion)
+	}
+	return gv.WithKind(object.GetKind())
+}
+
 func (r *RGFilter) Filter(object *yaml.RNode) (*yaml.RNode, error) {
-	if object.GetApiVersion() != rgfilev1alpha1.RGFileAPIVersion ||
-		object.GetKind() != rgfilev1alpha1.RGFileKind {
+	if GroupVersionKindForObject(object) != rgfilev1alpha1.ResourceGroupGVK() {
 		return object, nil
 	}
 
diff --git a/pkg/live/rgpath.go b/pkg/live/rgpath.go
index 556a1736..7c2e712e 100644
--- a/pkg/live/rgpath.go
+++ b/pkg/live/rgpath.go
@@ -66,7 +66,7 @@ func (r *ResourceGroupPathManifestReader) Read() ([]*unstructured.Unstructured,
 		// Skip if current file is a ResourceGroup resource. We do not want to apply/delete any ResourceGroup CRs when we
 		// run any `kpt live` commands on a package. Instead, we have specific logic in place for handling ResourceGroups in
 		// the live cluster.
-		if u.GetKind() == rgfilev1alpha1.RGFileKind && u.GetAPIVersion() == rgfilev1alpha1.DefaultMeta.APIVersion {
+		if u.GroupVersionKind() == rgfilev1alpha1.ResourceGroupGVK() {
 			continue
 		}
 		objs = append(objs, u)
diff --git a/pkg/live/rgstream.go b/pkg/live/rgstream.go
index 855b7568..354e3eb3 100644
--- a/pkg/live/rgstream.go
+++ b/pkg/live/rgstream.go
@@ -31,14 +31,8 @@ import (
 
 var (
 	excludedGKs = []schema.GroupKind{
-		{
-			Group: kptfilev1.KptFileGroup,
-			Kind:  kptfilev1.KptFileKind,
-		},
-		{
-			Group: rgfilev1alpha1.RGFileGroup,
-			Kind:  rgfilev1alpha1.RGFileKind,
-		},
+		kptfilev1.KptFileGVK().GroupKind(),
+		rgfilev1alpha1.ResourceGroupGVK().GroupKind(),
 	}
 )
 
diff --git a/porch/api/generated/openapi/zz_generated.openapi.go b/porch/api/generated/openapi/zz_generated.openapi.go
index 0a0089e3..ecf07760 100644
--- a/porch/api/generated/openapi/zz_generated.openapi.go
+++ b/porch/api/generated/openapi/zz_generated.openapi.go
@@ -29,6 +29,7 @@ import (
 
 func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
 	return map[string]common.OpenAPIDefinition{
+		"github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.Condition":                    schema_porch_api_porch_v1alpha1_Condition(ref),
 		"github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.Function":                     schema_porch_api_porch_v1alpha1_Function(ref),
 		"github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.FunctionConfig":               schema_porch_api_porch_v1alpha1_FunctionConfig(ref),
 		"github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.FunctionEvalTaskSpec":         schema_porch_api_porch_v1alpha1_FunctionEvalTaskSpec(ref),
@@ -58,6 +59,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
 		"github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.PackageUpdateTaskSpec":        schema_porch_api_porch_v1alpha1_PackageUpdateTaskSpec(ref),
 		"github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.ParentReference":              schema_porch_api_porch_v1alpha1_ParentReference(ref),
 		"github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.PatchSpec":                    schema_porch_api_porch_v1alpha1_PatchSpec(ref),
+		"github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.ReadinessGate":                schema_porch_api_porch_v1alpha1_ReadinessGate(ref),
 		"github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.RepositoryRef":                schema_porch_api_porch_v1alpha1_RepositoryRef(ref),
 		"github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.SecretRef":                    schema_porch_api_porch_v1alpha1_SecretRef(ref),
 		"github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.Selector":                     schema_porch_api_porch_v1alpha1_Selector(ref),
@@ -119,6 +121,45 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
 	}
 }
 
+func schema_porch_api_porch_v1alpha1_Condition(ref common.ReferenceCallback) common.OpenAPIDefinition {
+	return common.OpenAPIDefinition{
+		Schema: spec.Schema{
+			SchemaProps: spec.SchemaProps{
+				Type: []string{"object"},
+				Properties: map[string]spec.Schema{
+					"type": {
+						SchemaProps: spec.SchemaProps{
+							Default: "",
+							Type:    []string{"string"},
+							Format:  "",
+						},
+					},
+					"status": {
+						SchemaProps: spec.SchemaProps{
+							Default: "",
+							Type:    []string{"string"},
+							Format:  "",
+						},
+					},
+					"reason": {
+						SchemaProps: spec.SchemaProps{
+							Type:   []string{"string"},
+							Format: "",
+						},
+					},
+					"message": {
+						SchemaProps: spec.SchemaProps{
+							Type:   []string{"string"},
+							Format: "",
+						},
+					},
+				},
+				Required: []string{"type", "status"},
+			},
+		},
+	}
+}
+
 func schema_porch_api_porch_v1alpha1_Function(ref common.ReferenceCallback) common.OpenAPIDefinition {
 	return common.OpenAPIDefinition{
 		Schema: spec.Schema{
@@ -1097,11 +1138,24 @@ func schema_porch_api_porch_v1alpha1_PackageRevisionSpec(ref common.ReferenceCal
 							},
 						},
 					},
+					"readinessGates": {
+						SchemaProps: spec.SchemaProps{
+							Type: []string{"array"},
+							Items: &spec.SchemaOrArray{
+								Schema: &spec.Schema{
+									SchemaProps: spec.SchemaProps{
+										Default: map[string]interface{}{},
+										Ref:     ref("github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.ReadinessGate"),
+									},
+								},
+							},
+						},
+					},
 				},
 			},
 		},
 		Dependencies: []string{
-			"github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.ParentReference", "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.Task"},
+			"github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.ParentReference", "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.ReadinessGate", "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.Task"},
 	}
 }
 
@@ -1138,11 +1192,24 @@ func schema_porch_api_porch_v1alpha1_PackageRevisionStatus(ref common.ReferenceC
 							Format:      "",
 						},
 					},
+					"conditions": {
+						SchemaProps: spec.SchemaProps{
+							Type: []string{"array"},
+							Items: &spec.SchemaOrArray{
+								Schema: &spec.Schema{
+									SchemaProps: spec.SchemaProps{
+										Default: map[string]interface{}{},
+										Ref:     ref("github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.Condition"),
+									},
+								},
+							},
+						},
+					},
 				},
 			},
 		},
 		Dependencies: []string{
-			"github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.UpstreamLock", "k8s.io/apimachinery/pkg/apis/meta/v1.Time"},
+			"github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.Condition", "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.UpstreamLock", "k8s.io/apimachinery/pkg/apis/meta/v1.Time"},
 	}
 }
 
@@ -1266,6 +1333,24 @@ func schema_porch_api_porch_v1alpha1_PatchSpec(ref common.ReferenceCallback) com
 	}
 }
 
+func schema_porch_api_porch_v1alpha1_ReadinessGate(ref common.ReferenceCallback) common.OpenAPIDefinition {
+	return common.OpenAPIDefinition{
+		Schema: spec.Schema{
+			SchemaProps: spec.SchemaProps{
+				Type: []string{"object"},
+				Properties: map[string]spec.Schema{
+					"conditionType": {
+						SchemaProps: spec.SchemaProps{
+							Type:   []string{"string"},
+							Format: "",
+						},
+					},
+				},
+			},
+		},
+	}
+}
+
 func schema_porch_api_porch_v1alpha1_RepositoryRef(ref common.ReferenceCallback) common.OpenAPIDefinition {
 	return common.OpenAPIDefinition{
 		Schema: spec.Schema{
diff --git a/porch/api/porch/types_packagerevisions.go b/porch/api/porch/types_packagerevisions.go
index d9cc2ab4..02978353 100644
--- a/porch/api/porch/types_packagerevisions.go
+++ b/porch/api/porch/types_packagerevisions.go
@@ -66,6 +66,12 @@ type PackageRevisionSpec struct {
 	Lifecycle PackageRevisionLifecycle `json:"lifecycle,omitempty"`
 
 	Tasks []Task `json:"tasks,omitempty"`
+
+	ReadinessGates []ReadinessGate `json:"readinessGates,omitempty"`
+}
+
+type ReadinessGate struct {
+	ConditionType string `json:"conditionType,omitempty"`
 }
 
 // ParentReference is a reference to a parent package
@@ -88,6 +94,8 @@ type PackageRevisionStatus struct {
 
 	// Deployment is true if this is a deployment package (in a deployment repository).
 	Deployment bool `json:"deployment,omitempty"`
+
+	Conditions []Condition `json:"conditions,omitempty"`
 }
 
 type TaskType string
@@ -302,3 +310,21 @@ type GitLock struct {
 	// This is set by kpt for bookkeeping purposes.
 	Commit string `yaml:"commit,omitempty" json:"commit,omitempty"`
 }
+
+type Condition struct {
+	Type string `yaml:"type" json:"type"`
+
+	Status ConditionStatus `yaml:"status" json:"status"`
+
+	Reason string `yaml:"reason,omitempty" json:"reason,omitempty"`
+
+	Message string `yaml:"message,omitempty" json:"message,omitempty"`
+}
+
+type ConditionStatus string
+
+const (
+	ConditionTrue    ConditionStatus = "True"
+	ConditionFalse   ConditionStatus = "False"
+	ConditionUnknown ConditionStatus = "Unknown"
+)
diff --git a/porch/api/porch/v1alpha1/types_packagerevisions.go b/porch/api/porch/v1alpha1/types_packagerevisions.go
index b3f755fe..6447bf49 100644
--- a/porch/api/porch/v1alpha1/types_packagerevisions.go
+++ b/porch/api/porch/v1alpha1/types_packagerevisions.go
@@ -74,6 +74,12 @@ type PackageRevisionSpec struct {
 	Lifecycle PackageRevisionLifecycle `json:"lifecycle,omitempty"`
 
 	Tasks []Task `json:"tasks,omitempty"`
+
+	ReadinessGates []ReadinessGate `json:"readinessGates,omitempty"`
+}
+
+type ReadinessGate struct {
+	ConditionType string `json:"conditionType,omitempty"`
 }
 
 // ParentReference is a reference to a parent package
@@ -96,6 +102,8 @@ type PackageRevisionStatus struct {
 
 	// Deployment is true if this is a deployment package (in a deployment repository).
 	Deployment bool `json:"deployment,omitempty"`
+
+	Conditions []Condition `json:"conditions,omitempty"`
 }
 
 type TaskType string
@@ -313,3 +321,21 @@ type GitLock struct {
 	// This is set by kpt for bookkeeping purposes.
 	Commit string `yaml:"commit,omitempty" json:"commit,omitempty"`
 }
+
+type Condition struct {
+	Type string `yaml:"type" json:"type"`
+
+	Status ConditionStatus `yaml:"status" json:"status"`
+
+	Reason string `yaml:"reason,omitempty" json:"reason,omitempty"`
+
+	Message string `yaml:"message,omitempty" json:"message,omitempty"`
+}
+
+type ConditionStatus string
+
+const (
+	ConditionTrue    ConditionStatus = "True"
+	ConditionFalse   ConditionStatus = "False"
+	ConditionUnknown ConditionStatus = "Unknown"
+)
diff --git a/porch/api/porch/v1alpha1/zz_generated.conversion.go b/porch/api/porch/v1alpha1/zz_generated.conversion.go
index 4de34089..b29424f4 100644
--- a/porch/api/porch/v1alpha1/zz_generated.conversion.go
+++ b/porch/api/porch/v1alpha1/zz_generated.conversion.go
@@ -34,6 +34,16 @@ func init() {
 // RegisterConversions adds conversion functions to the given scheme.
 // Public to allow building arbitrary schemes.
 func RegisterConversions(s *runtime.Scheme) error {
+	if err := s.AddGeneratedConversionFunc((*Condition)(nil), (*porch.Condition)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_v1alpha1_Condition_To_porch_Condition(a.(*Condition), b.(*porch.Condition), scope)
+	}); err != nil {
+		return err
+	}
+	if err := s.AddGeneratedConversionFunc((*porch.Condition)(nil), (*Condition)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_porch_Condition_To_v1alpha1_Condition(a.(*porch.Condition), b.(*Condition), scope)
+	}); err != nil {
+		return err
+	}
 	if err := s.AddGeneratedConversionFunc((*Function)(nil), (*porch.Function)(nil), func(a, b interface{}, scope conversion.Scope) error {
 		return Convert_v1alpha1_Function_To_porch_Function(a.(*Function), b.(*porch.Function), scope)
 	}); err != nil {
@@ -324,6 +334,16 @@ func RegisterConversions(s *runtime.Scheme) error {
 	}); err != nil {
 		return err
 	}
+	if err := s.AddGeneratedConversionFunc((*ReadinessGate)(nil), (*porch.ReadinessGate)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_v1alpha1_ReadinessGate_To_porch_ReadinessGate(a.(*ReadinessGate), b.(*porch.ReadinessGate), scope)
+	}); err != nil {
+		return err
+	}
+	if err := s.AddGeneratedConversionFunc((*porch.ReadinessGate)(nil), (*ReadinessGate)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_porch_ReadinessGate_To_v1alpha1_ReadinessGate(a.(*porch.ReadinessGate), b.(*ReadinessGate), scope)
+	}); err != nil {
+		return err
+	}
 	if err := s.AddGeneratedConversionFunc((*RepositoryRef)(nil), (*porch.RepositoryRef)(nil), func(a, b interface{}, scope conversion.Scope) error {
 		return Convert_v1alpha1_RepositoryRef_To_porch_RepositoryRef(a.(*RepositoryRef), b.(*porch.RepositoryRef), scope)
 	}); err != nil {
@@ -387,6 +407,32 @@ func RegisterConversions(s *runtime.Scheme) error {
 	return nil
 }
 
+func autoConvert_v1alpha1_Condition_To_porch_Condition(in *Condition, out *porch.Condition, s conversion.Scope) error {
+	out.Type = in.Type
+	out.Status = porch.ConditionStatus(in.Status)
+	out.Reason = in.Reason
+	out.Message = in.Message
+	return nil
+}
+
+// Convert_v1alpha1_Condition_To_porch_Condition is an autogenerated conversion function.
+func Convert_v1alpha1_Condition_To_porch_Condition(in *Condition, out *porch.Condition, s conversion.Scope) error {
+	return autoConvert_v1alpha1_Condition_To_porch_Condition(in, out, s)
+}
+
+func autoConvert_porch_Condition_To_v1alpha1_Condition(in *porch.Condition, out *Condition, s conversion.Scope) error {
+	out.Type = in.Type
+	out.Status = ConditionStatus(in.Status)
+	out.Reason = in.Reason
+	out.Message = in.Message
+	return nil
+}
+
+// Convert_porch_Condition_To_v1alpha1_Condition is an autogenerated conversion function.
+func Convert_porch_Condition_To_v1alpha1_Condition(in *porch.Condition, out *Condition, s conversion.Scope) error {
+	return autoConvert_porch_Condition_To_v1alpha1_Condition(in, out, s)
+}
+
 func autoConvert_v1alpha1_Function_To_porch_Function(in *Function, out *porch.Function, s conversion.Scope) error {
 	out.ObjectMeta = in.ObjectMeta
 	if err := Convert_v1alpha1_FunctionSpec_To_porch_FunctionSpec(&in.Spec, &out.Spec, s); err != nil {
@@ -950,6 +996,7 @@ func autoConvert_v1alpha1_PackageRevisionSpec_To_porch_PackageRevisionSpec(in *P
 	out.Parent = (*porch.ParentReference)(unsafe.Pointer(in.Parent))
 	out.Lifecycle = porch.PackageRevisionLifecycle(in.Lifecycle)
 	out.Tasks = *(*[]porch.Task)(unsafe.Pointer(&in.Tasks))
+	out.ReadinessGates = *(*[]porch.ReadinessGate)(unsafe.Pointer(&in.ReadinessGates))
 	return nil
 }
 
@@ -965,6 +1012,7 @@ func autoConvert_porch_PackageRevisionSpec_To_v1alpha1_PackageRevisionSpec(in *p
 	out.Parent = (*ParentReference)(unsafe.Pointer(in.Parent))
 	out.Lifecycle = PackageRevisionLifecycle(in.Lifecycle)
 	out.Tasks = *(*[]Task)(unsafe.Pointer(&in.Tasks))
+	out.ReadinessGates = *(*[]ReadinessGate)(unsafe.Pointer(&in.ReadinessGates))
 	return nil
 }
 
@@ -978,6 +1026,7 @@ func autoConvert_v1alpha1_PackageRevisionStatus_To_porch_PackageRevisionStatus(i
 	out.PublishedBy = in.PublishedBy
 	out.PublishedAt = in.PublishedAt
 	out.Deployment = in.Deployment
+	out.Conditions = *(*[]porch.Condition)(unsafe.Pointer(&in.Conditions))
 	return nil
 }
 
@@ -991,6 +1040,7 @@ func autoConvert_porch_PackageRevisionStatus_To_v1alpha1_PackageRevisionStatus(i
 	out.PublishedBy = in.PublishedBy
 	out.PublishedAt = in.PublishedAt
 	out.Deployment = in.Deployment
+	out.Conditions = *(*[]Condition)(unsafe.Pointer(&in.Conditions))
 	return nil
 }
 
@@ -1109,6 +1159,26 @@ func Convert_porch_PatchSpec_To_v1alpha1_PatchSpec(in *porch.PatchSpec, out *Pat
 	return autoConvert_porch_PatchSpec_To_v1alpha1_PatchSpec(in, out, s)
 }
 
+func autoConvert_v1alpha1_ReadinessGate_To_porch_ReadinessGate(in *ReadinessGate, out *porch.ReadinessGate, s conversion.Scope) error {
+	out.ConditionType = in.ConditionType
+	return nil
+}
+
+// Convert_v1alpha1_ReadinessGate_To_porch_ReadinessGate is an autogenerated conversion function.
+func Convert_v1alpha1_ReadinessGate_To_porch_ReadinessGate(in *ReadinessGate, out *porch.ReadinessGate, s conversion.Scope) error {
+	return autoConvert_v1alpha1_ReadinessGate_To_porch_ReadinessGate(in, out, s)
+}
+
+func autoConvert_porch_ReadinessGate_To_v1alpha1_ReadinessGate(in *porch.ReadinessGate, out *ReadinessGate, s conversion.Scope) error {
+	out.ConditionType = in.ConditionType
+	return nil
+}
+
+// Convert_porch_ReadinessGate_To_v1alpha1_ReadinessGate is an autogenerated conversion function.
+func Convert_porch_ReadinessGate_To_v1alpha1_ReadinessGate(in *porch.ReadinessGate, out *ReadinessGate, s conversion.Scope) error {
+	return autoConvert_porch_ReadinessGate_To_v1alpha1_ReadinessGate(in, out, s)
+}
+
 func autoConvert_v1alpha1_RepositoryRef_To_porch_RepositoryRef(in *RepositoryRef, out *porch.RepositoryRef, s conversion.Scope) error {
 	out.Name = in.Name
 	return nil
diff --git a/porch/api/porch/v1alpha1/zz_generated.deepcopy.go b/porch/api/porch/v1alpha1/zz_generated.deepcopy.go
index 24cbb1ab..6b7440b6 100644
--- a/porch/api/porch/v1alpha1/zz_generated.deepcopy.go
+++ b/porch/api/porch/v1alpha1/zz_generated.deepcopy.go
@@ -23,6 +23,22 @@ import (
 	runtime "k8s.io/apimachinery/pkg/runtime"
 )
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Condition) DeepCopyInto(out *Condition) {
+	*out = *in
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition.
+func (in *Condition) DeepCopy() *Condition {
+	if in == nil {
+		return nil
+	}
+	out := new(Condition)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Function) DeepCopyInto(out *Function) {
 	*out = *in
@@ -567,6 +583,11 @@ func (in *PackageRevisionSpec) DeepCopyInto(out *PackageRevisionSpec) {
 			(*in)[i].DeepCopyInto(&(*out)[i])
 		}
 	}
+	if in.ReadinessGates != nil {
+		in, out := &in.ReadinessGates, &out.ReadinessGates
+		*out = make([]ReadinessGate, len(*in))
+		copy(*out, *in)
+	}
 	return
 }
 
@@ -589,6 +610,11 @@ func (in *PackageRevisionStatus) DeepCopyInto(out *PackageRevisionStatus) {
 		(*in).DeepCopyInto(*out)
 	}
 	in.PublishedAt.DeepCopyInto(&out.PublishedAt)
+	if in.Conditions != nil {
+		in, out := &in.Conditions, &out.Conditions
+		*out = make([]Condition, len(*in))
+		copy(*out, *in)
+	}
 	return
 }
 
@@ -683,6 +709,22 @@ func (in *PatchSpec) DeepCopy() *PatchSpec {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ReadinessGate) DeepCopyInto(out *ReadinessGate) {
+	*out = *in
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReadinessGate.
+func (in *ReadinessGate) DeepCopy() *ReadinessGate {
+	if in == nil {
+		return nil
+	}
+	out := new(ReadinessGate)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *RepositoryRef) DeepCopyInto(out *RepositoryRef) {
 	*out = *in
diff --git a/porch/api/porch/zz_generated.deepcopy.go b/porch/api/porch/zz_generated.deepcopy.go
index cd31543f..c66dd4dd 100644
--- a/porch/api/porch/zz_generated.deepcopy.go
+++ b/porch/api/porch/zz_generated.deepcopy.go
@@ -23,6 +23,22 @@ import (
 	runtime "k8s.io/apimachinery/pkg/runtime"
 )
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Condition) DeepCopyInto(out *Condition) {
+	*out = *in
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition.
+func (in *Condition) DeepCopy() *Condition {
+	if in == nil {
+		return nil
+	}
+	out := new(Condition)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Function) DeepCopyInto(out *Function) {
 	*out = *in
@@ -567,6 +583,11 @@ func (in *PackageRevisionSpec) DeepCopyInto(out *PackageRevisionSpec) {
 			(*in)[i].DeepCopyInto(&(*out)[i])
 		}
 	}
+	if in.ReadinessGates != nil {
+		in, out := &in.ReadinessGates, &out.ReadinessGates
+		*out = make([]ReadinessGate, len(*in))
+		copy(*out, *in)
+	}
 	return
 }
 
@@ -589,6 +610,11 @@ func (in *PackageRevisionStatus) DeepCopyInto(out *PackageRevisionStatus) {
 		(*in).DeepCopyInto(*out)
 	}
 	in.PublishedAt.DeepCopyInto(&out.PublishedAt)
+	if in.Conditions != nil {
+		in, out := &in.Conditions, &out.Conditions
+		*out = make([]Condition, len(*in))
+		copy(*out, *in)
+	}
 	return
 }
 
@@ -683,6 +709,22 @@ func (in *PatchSpec) DeepCopy() *PatchSpec {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ReadinessGate) DeepCopyInto(out *ReadinessGate) {
+	*out = *in
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReadinessGate.
+func (in *ReadinessGate) DeepCopy() *ReadinessGate {
+	if in == nil {
+		return nil
+	}
+	out := new(ReadinessGate)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *RepositoryRef) DeepCopyInto(out *RepositoryRef) {
 	*out = *in
diff --git a/porch/pkg/cache/packagerevision.go b/porch/pkg/cache/packagerevision.go
index a89e14d6..a4aa5133 100644
--- a/porch/pkg/cache/packagerevision.go
+++ b/porch/pkg/cache/packagerevision.go
@@ -15,6 +15,8 @@
 package cache
 
 import (
+	"context"
+
 	"github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1"
 	"github.com/GoogleContainerTools/kpt/porch/pkg/repository"
 )
@@ -32,13 +34,16 @@ type cachedPackageRevision struct {
 	isLatestRevision bool
 }
 
-func (c *cachedPackageRevision) GetPackageRevision() *v1alpha1.PackageRevision {
-	rev := c.PackageRevision.GetPackageRevision()
+func (c *cachedPackageRevision) GetPackageRevision(ctx context.Context) (*v1alpha1.PackageRevision, error) {
+	rev, err := c.PackageRevision.GetPackageRevision(ctx)
+	if err != nil {
+		return nil, err
+	}
 	if c.isLatestRevision {
 		if rev.Labels == nil {
 			rev.Labels = map[string]string{}
 		}
 		rev.Labels[v1alpha1.LatestPackageRevisionKey] = v1alpha1.LatestPackageRevisionValue
 	}
-	return rev
+	return rev, nil
 }
diff --git a/porch/pkg/engine/engine.go b/porch/pkg/engine/engine.go
index f6faf922..657ffd4c 100644
--- a/porch/pkg/engine/engine.go
+++ b/porch/pkg/engine/engine.go
@@ -15,6 +15,7 @@
 package engine
 
 import (
+	"bytes"
 	"context"
 	"fmt"
 	"io/fs"
@@ -22,6 +23,7 @@ import (
 	"path/filepath"
 	"reflect"
 
+	kptfile "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1"
 	"github.com/GoogleContainerTools/kpt/pkg/fn"
 	api "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1"
 	configapi "github.com/GoogleContainerTools/kpt/porch/api/porchconfig/v1alpha1"
@@ -76,8 +78,11 @@ type PackageRevision struct {
 	packageRevisionMeta meta.PackageRevisionMeta
 }
 
-func (p *PackageRevision) GetPackageRevision() *api.PackageRevision {
-	repoPkgRev := p.repoPackageRevision.GetPackageRevision()
+func (p *PackageRevision) GetPackageRevision(ctx context.Context) (*api.PackageRevision, error) {
+	repoPkgRev, err := p.repoPackageRevision.GetPackageRevision(ctx)
+	if err != nil {
+		return nil, err
+	}
 	var isLatest bool
 	if val, found := repoPkgRev.Labels[api.LatestPackageRevisionKey]; found && val == api.LatestPackageRevisionValue {
 		isLatest = true
@@ -90,7 +95,7 @@ func (p *PackageRevision) GetPackageRevision() *api.PackageRevision {
 		repoPkgRev.Labels[api.LatestPackageRevisionKey] = api.LatestPackageRevisionValue
 	}
 	repoPkgRev.Annotations = p.packageRevisionMeta.Annotations
-	return repoPkgRev
+	return repoPkgRev, nil
 }
 
 func (p *PackageRevision) KubeObjectName() string {
@@ -458,6 +463,23 @@ func (cad *cadEngine) UpdatePackageRevision(ctx context.Context, repositoryObj *
 		return nil, err
 	}
 
+	// If any of the fields in the API that are projections from the Kptfile
+	// must be updated in the Kptfile as well.
+	kfPatchTask, created, err := createKptfilePatchTask(ctx, oldPackage.repoPackageRevision, newObj)
+	if err != nil {
+		return nil, err
+	}
+	if created {
+		kfPatchMutation, err := buildPatchMutation(ctx, kfPatchTask)
+		if err != nil {
+			return nil, err
+		}
+		mutations = append(mutations, kfPatchMutation)
+	}
+
+	// Re-render if we are making changes.
+	mutations = cad.conditionalAddRender(mutations)
+
 	// TODO: Handle the case if alongside lifecycle change, tasks are changed too.
 	// Update package contents only if the package is in draft state
 	if oldObj.Spec.Lifecycle == api.PackageRevisionLifecycleDraft {
@@ -498,6 +520,95 @@ func (cad *cadEngine) UpdatePackageRevision(ctx context.Context, repositoryObj *
 	}, nil
 }
 
+func createKptfilePatchTask(ctx context.Context, oldPackage repository.PackageRevision, newObj *api.PackageRevision) (*api.Task, bool, error) {
+	kf, err := oldPackage.GetKptfile(ctx)
+	if err != nil {
+		return nil, false, err
+	}
+
+	var orgKfString string
+	{
+		var buf bytes.Buffer
+		d := yaml.NewEncoder(&buf)
+		if err := d.Encode(kf); err != nil {
+			return nil, false, err
+		}
+		orgKfString = buf.String()
+	}
+
+	var readinessGates []kptfile.ReadinessGate
+	for _, rg := range newObj.Spec.ReadinessGates {
+		readinessGates = append(readinessGates, kptfile.ReadinessGate{
+			ConditionType: rg.ConditionType,
+		})
+	}
+
+	var conditions []kptfile.Condition
+	for _, c := range newObj.Status.Conditions {
+		conditions = append(conditions, kptfile.Condition{
+			Type:    c.Type,
+			Status:  convertStatusToKptfile(c.Status),
+			Reason:  c.Reason,
+			Message: c.Message,
+		})
+	}
+
+	if kf.Info == nil && len(readinessGates) > 0 {
+		kf.Info = &kptfile.PackageInfo{}
+	}
+	if len(readinessGates) > 0 {
+		kf.Info.ReadinessGates = readinessGates
+	}
+
+	if kf.Status == nil && len(conditions) > 0 {
+		kf.Status = &kptfile.Status{}
+	}
+	if len(conditions) > 0 {
+		kf.Status.Conditions = conditions
+	}
+
+	var newKfString string
+	{
+		var buf bytes.Buffer
+		d := yaml.NewEncoder(&buf)
+		if err := d.Encode(kf); err != nil {
+			return nil, false, err
+		}
+		newKfString = buf.String()
+	}
+
+	patchSpec, err := GeneratePatch(kptfile.KptFileName, orgKfString, newKfString)
+	if err != nil {
+		return nil, false, err
+	}
+	// If patch is empty, don't create a Task.
+	if patchSpec.Contents == "" {
+		return nil, false, nil
+	}
+
+	return &api.Task{
+		Type: api.TaskTypePatch,
+		Patch: &api.PackagePatchTaskSpec{
+			Patches: []api.PatchSpec{
+				patchSpec,
+			},
+		},
+	}, true, nil
+}
+
+func convertStatusToKptfile(s api.ConditionStatus) kptfile.ConditionStatus {
+	switch s {
+	case api.ConditionTrue:
+		return kptfile.ConditionTrue
+	case api.ConditionFalse:
+		return kptfile.ConditionFalse
+	case api.ConditionUnknown:
+		return kptfile.ConditionUnknown
+	default:
+		panic(fmt.Errorf("unknown condition status: %v", s))
+	}
+}
+
 // conditionalAddRender adds a render mutation to the end of the mutations slice if the last
 // entry is not already a render mutation.
 func (cad *cadEngine) conditionalAddRender(mutations []mutation) []mutation {
@@ -611,7 +722,10 @@ func (cad *cadEngine) UpdatePackageResources(ctx context.Context, repositoryObj
 	ctx, span := tracer.Start(ctx, "cadEngine::UpdatePackageResources", trace.WithAttributes())
 	defer span.End()
 
-	rev := oldPackage.repoPackageRevision.GetPackageRevision()
+	rev, err := oldPackage.repoPackageRevision.GetPackageRevision(ctx)
+	if err != nil {
+		return nil, err
+	}
 
 	// Validate package lifecycle. Can only update a draft.
 	switch lifecycle := rev.Spec.Lifecycle; lifecycle {
diff --git a/porch/pkg/engine/fake/packagerevision.go b/porch/pkg/engine/fake/packagerevision.go
index 2cfe5fa9..290086c0 100644
--- a/porch/pkg/engine/fake/packagerevision.go
+++ b/porch/pkg/engine/fake/packagerevision.go
@@ -30,8 +30,7 @@ type PackageRevision struct {
 	PackageLifecycle   v1alpha1.PackageRevisionLifecycle
 	PackageRevision    *v1alpha1.PackageRevision
 	Resources          *v1alpha1.PackageRevisionResources
-	Upstream           kptfile.Upstream
-	UpstreamLock       kptfile.UpstreamLock
+	Kptfile            kptfile.KptFile
 }
 
 func (pr *PackageRevision) KubeObjectName() string {
@@ -50,18 +49,22 @@ func (pr *PackageRevision) Lifecycle() v1alpha1.PackageRevisionLifecycle {
 	return pr.PackageLifecycle
 }
 
-func (pr *PackageRevision) GetPackageRevision() *v1alpha1.PackageRevision {
-	return nil
+func (pr *PackageRevision) GetPackageRevision(context.Context) (*v1alpha1.PackageRevision, error) {
+	return nil, nil
 }
 
 func (f *PackageRevision) GetResources(context.Context) (*v1alpha1.PackageRevisionResources, error) {
 	return f.Resources, nil
 }
 
-func (f *PackageRevision) GetUpstreamLock() (kptfile.Upstream, kptfile.UpstreamLock, error) {
-	return kptfile.Upstream{}, kptfile.UpstreamLock{}, nil
+func (f *PackageRevision) GetKptfile(ctx context.Context) (kptfile.KptFile, error) {
+	return f.Kptfile, nil
+}
+
+func (f *PackageRevision) GetUpstreamLock(context.Context) (kptfile.Upstream, kptfile.UpstreamLock, error) {
+	return *f.Kptfile.Upstream, *f.Kptfile.UpstreamLock, nil
 }
 
 func (f *PackageRevision) GetLock() (kptfile.Upstream, kptfile.UpstreamLock, error) {
-	return kptfile.Upstream{}, kptfile.UpstreamLock{}, nil
+	return *f.Kptfile.Upstream, *f.Kptfile.UpstreamLock, nil
 }
diff --git a/porch/pkg/git/git.go b/porch/pkg/git/git.go
index aaaf38ab..0f79f263 100644
--- a/porch/pkg/git/git.go
+++ b/porch/pkg/git/git.go
@@ -962,6 +962,35 @@ func (r *gitRepository) loadTasks(ctx context.Context, startCommit *object.Commi
 	return tasks, nil
 }
 
+func (r *gitRepository) getResources(hash plumbing.Hash) (map[string]string, error) {
+	resources := map[string]string{}
+
+	tree, err := r.repo.TreeObject(hash)
+	if err == nil {
+		// Files() iterator iterates recursively over all files in the tree.
+		fit := tree.Files()
+		defer fit.Close()
+		for {
+			file, err := fit.Next()
+			if err == io.EOF {
+				break
+			} else if err != nil {
+				return nil, fmt.Errorf("failed to load package resources: %w", err)
+			}
+
+			content, err := file.Contents()
+			if err != nil {
+				return nil, fmt.Errorf("failed to read package file contents: %q, %w", file.Name, err)
+			}
+
+			// TODO: decide whether paths should include package directory or not.
+			resources[file.Name] = content
+			//resources[path.Join(p.path, file.Name)] = content
+		}
+	}
+	return resources, nil
+}
+
 // findLatestPackageCommit returns the latest commit from the history that pertains
 // to the package given by the packagePath. If no commit is found, it will return nil.
 func (r *gitRepository) findLatestPackageCommit(ctx context.Context, startCommit *object.Commit, packagePath string) (*object.Commit, error) {
diff --git a/porch/pkg/git/package.go b/porch/pkg/git/package.go
index e4b6c278..99179934 100644
--- a/porch/pkg/git/package.go
+++ b/porch/pkg/git/package.go
@@ -19,7 +19,6 @@ import (
 	"crypto/sha1"
 	"encoding/hex"
 	"fmt"
-	"io"
 	"strings"
 	"time"
 
@@ -75,10 +74,10 @@ func (p *gitPackageRevision) uid() types.UID {
 	return types.UID(fmt.Sprintf("uid:%s:%s", p.path, p.revision))
 }
 
-func (p *gitPackageRevision) GetPackageRevision() *v1alpha1.PackageRevision {
+func (p *gitPackageRevision) GetPackageRevision(ctx context.Context) (*v1alpha1.PackageRevision, error) {
 	key := p.Key()
 
-	_, lock, _ := p.GetUpstreamLock()
+	_, lock, _ := p.GetUpstreamLock(ctx)
 
 	lockCopy := &v1alpha1.UpstreamLock{}
 	// TODO: Use kpt definition of UpstreamLock in the package revision status
@@ -96,10 +95,13 @@ func (p *gitPackageRevision) GetPackageRevision() *v1alpha1.PackageRevision {
 		}
 	}
 
+	kf, _ := p.GetKptfile(ctx)
+
 	status := v1alpha1.PackageRevisionStatus{
 		UpstreamLock: lockCopy,
+		Deployment:   p.repo.deployment,
+		Conditions:   repository.ToApiConditions(kf),
 	}
-	status.Deployment = p.repo.deployment
 
 	if p.Lifecycle() == v1alpha1.PackageRevisionLifecyclePublished {
 		if !p.updated.IsZero() {
@@ -129,38 +131,18 @@ func (p *gitPackageRevision) GetPackageRevision() *v1alpha1.PackageRevision {
 			Revision:       key.Revision,
 			RepositoryName: key.Repository,
 
-			Lifecycle: p.Lifecycle(),
-			Tasks:     p.tasks,
+			Lifecycle:      p.Lifecycle(),
+			Tasks:          p.tasks,
+			ReadinessGates: repository.ToApiReadinessGates(kf),
 		},
 		Status: status,
-	}
+	}, nil
 }
 
 func (p *gitPackageRevision) GetResources(ctx context.Context) (*v1alpha1.PackageRevisionResources, error) {
-	resources := map[string]string{}
-
-	tree, err := p.repo.repo.TreeObject(p.tree)
-	if err == nil {
-		// Files() iterator iterates recursively over all files in the tree.
-		fit := tree.Files()
-		defer fit.Close()
-		for {
-			file, err := fit.Next()
-			if err == io.EOF {
-				break
-			} else if err != nil {
-				return nil, fmt.Errorf("failed to load package resources: %w", err)
-			}
-
-			content, err := file.Contents()
-			if err != nil {
-				return nil, fmt.Errorf("failed to read package file contents: %q, %w", file.Name, err)
-			}
-
-			// TODO: decide whether paths should include package directory or not.
-			resources[file.Name] = content
-			//resources[path.Join(p.path, file.Name)] = content
-		}
+	resources, err := p.repo.getResources(p.tree)
+	if err != nil {
+		return nil, fmt.Errorf("failed to load package resources: %w", err)
 	}
 
 	key := p.Key()
@@ -190,20 +172,27 @@ func (p *gitPackageRevision) GetResources(ctx context.Context) (*v1alpha1.Packag
 	}, nil
 }
 
-// GetUpstreamLock returns the upstreamLock info present in the Kptfile of the package.
-func (p *gitPackageRevision) GetUpstreamLock() (kptfile.Upstream, kptfile.UpstreamLock, error) {
-	resources, err := p.GetResources(context.Background())
+func (p *gitPackageRevision) GetKptfile(ctx context.Context) (kptfile.KptFile, error) {
+	resources, err := p.repo.getResources(p.tree)
 	if err != nil {
-		return kptfile.Upstream{}, kptfile.UpstreamLock{}, fmt.Errorf("cannot determine package lock; cannot retrieve resources: %w", err)
+		return kptfile.KptFile{}, fmt.Errorf("error loading package resources: %w", err)
+	}
+	kfString, found := resources[kptfile.KptFileName]
+	if !found {
+		return kptfile.KptFile{}, fmt.Errorf("packagerevision does not have a Kptfile")
 	}
+	kf, err := pkg.DecodeKptfile(strings.NewReader(kfString))
+	if err != nil {
+		return kptfile.KptFile{}, fmt.Errorf("error decoding Kptfile: %w", err)
+	}
+	return *kf, nil
+}
 
-	// get the upstream package URL from the Kptfile
-	var kf *kptfile.KptFile
-	if contents, found := resources.Spec.Resources[kptfile.KptFileName]; found {
-		kf, err = pkg.DecodeKptfile(strings.NewReader(contents))
-		if err != nil {
-			return kptfile.Upstream{}, kptfile.UpstreamLock{}, fmt.Errorf("cannot decode Kptfile: %w", err)
-		}
+// GetUpstreamLock returns the upstreamLock info present in the Kptfile of the package.
+func (p *gitPackageRevision) GetUpstreamLock(ctx context.Context) (kptfile.Upstream, kptfile.UpstreamLock, error) {
+	kf, err := p.GetKptfile(ctx)
+	if err != nil {
+		return kptfile.Upstream{}, kptfile.UpstreamLock{}, fmt.Errorf("cannot determine package lock; cannot retrieve resources: %w", err)
 	}
 
 	if kf.Upstream == nil || kf.UpstreamLock == nil || kf.Upstream.Git == nil {
diff --git a/porch/pkg/oci/oci.go b/porch/pkg/oci/oci.go
index 8936d4d4..9a3d1e05 100644
--- a/porch/pkg/oci/oci.go
+++ b/porch/pkg/oci/oci.go
@@ -19,8 +19,10 @@ import (
 	"crypto/sha1"
 	"encoding/hex"
 	"fmt"
+	"strings"
 	"time"
 
+	"github.com/GoogleContainerTools/kpt/internal/pkg"
 	kptfile "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1"
 	"github.com/GoogleContainerTools/kpt/pkg/oci"
 	"github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1"
@@ -383,9 +385,14 @@ func (p *ociPackageRevision) Key() repository.PackageRevisionKey {
 	}
 }
 
-func (p *ociPackageRevision) GetPackageRevision() *v1alpha1.PackageRevision {
+func (p *ociPackageRevision) GetPackageRevision(ctx context.Context) (*v1alpha1.PackageRevision, error) {
 	key := p.Key()
 
+	kf, err := p.GetKptfile(ctx)
+	if err != nil {
+		return nil, err
+	}
+
 	return &v1alpha1.PackageRevision{
 		TypeMeta: metav1.TypeMeta{
 			Kind:       "PackageRevision",
@@ -405,17 +412,35 @@ func (p *ociPackageRevision) GetPackageRevision() *v1alpha1.PackageRevision {
 			Revision:       key.Revision,
 			RepositoryName: key.Repository,
 
-			Lifecycle: p.Lifecycle(),
-			Tasks:     p.tasks,
+			Lifecycle:      p.Lifecycle(),
+			Tasks:          p.tasks,
+			ReadinessGates: repository.ToApiReadinessGates(kf),
 		},
 		Status: v1alpha1.PackageRevisionStatus{
 			// TODO:        UpstreamLock,
 			Deployment: p.parent.deployment,
+			Conditions: repository.ToApiConditions(kf),
 		},
+	}, nil
+}
+
+func (p *ociPackageRevision) GetKptfile(ctx context.Context) (kptfile.KptFile, error) {
+	resources, err := LoadResources(ctx, p.parent.storage, &p.digestName)
+	if err != nil {
+		return kptfile.KptFile{}, fmt.Errorf("error loading package resources: %w", err)
+	}
+	kfString, found := resources.Contents[kptfile.KptFileName]
+	if !found {
+		return kptfile.KptFile{}, fmt.Errorf("packagerevision does not have a Kptfile")
+	}
+	kf, err := pkg.DecodeKptfile(strings.NewReader(kfString))
+	if err != nil {
+		return kptfile.KptFile{}, fmt.Errorf("error decoding Kptfile: %w", err)
 	}
+	return *kf, nil
 }
 
-func (p *ociPackageRevision) GetUpstreamLock() (kptfile.Upstream, kptfile.UpstreamLock, error) {
+func (p *ociPackageRevision) GetUpstreamLock(context.Context) (kptfile.Upstream, kptfile.UpstreamLock, error) {
 	return kptfile.Upstream{}, kptfile.UpstreamLock{}, fmt.Errorf("UpstreamLock is not supported for OCI packages (%s)", p.KubeObjectName())
 }
 
diff --git a/porch/pkg/registry/porch/packagecommon.go b/porch/pkg/registry/porch/packagecommon.go
index a3bd757d..627e1667 100644
--- a/porch/pkg/registry/porch/packagecommon.go
+++ b/porch/pkg/registry/porch/packagecommon.go
@@ -76,7 +76,11 @@ func (r *packageCommon) listPackageRevisions(ctx context.Context, filter package
 			return err
 		}
 		for _, rev := range revisions {
-			apiPkgRev := rev.GetPackageRevision()
+			apiPkgRev, err := rev.GetPackageRevision(ctx)
+			if err != nil {
+				return err
+			}
+
 			if selector != nil && !selector.Matches(labels.Set(apiPkgRev.Labels)) {
 				continue
 			}
@@ -218,7 +222,10 @@ func (r *packageCommon) updatePackageRevision(ctx context.Context, name string,
 
 	var oldApiPkgRev runtime.Object // We have to be runtime.Object (and not *api.PackageRevision) or else nil-checks fail (because a nil object is not a nil interface)
 	if !isCreate {
-		oldApiPkgRev = oldRepoPkgRev.GetPackageRevision()
+		oldApiPkgRev, err = oldRepoPkgRev.GetPackageRevision(ctx)
+		if err != nil {
+			return nil, false, err
+		}
 	}
 
 	newRuntimeObj, err := objInfo.UpdatedObject(ctx, oldApiPkgRev)
@@ -273,7 +280,11 @@ func (r *packageCommon) updatePackageRevision(ctx context.Context, name string,
 		if err != nil {
 			return nil, false, apierrors.NewInternalError(err)
 		}
-		updated := rev.GetPackageRevision()
+
+		updated, err := rev.GetPackageRevision(ctx)
+		if err != nil {
+			return nil, false, apierrors.NewInternalError(err)
+		}
 
 		return updated, false, nil
 	} else {
@@ -282,7 +293,10 @@ func (r *packageCommon) updatePackageRevision(ctx context.Context, name string,
 			klog.Infof("error creating package: %v", err)
 			return nil, false, apierrors.NewInternalError(err)
 		}
-		createdApiPkgRev := rev.GetPackageRevision()
+		createdApiPkgRev, err := rev.GetPackageRevision(ctx)
+		if err != nil {
+			return nil, false, apierrors.NewInternalError(err)
+		}
 
 		return createdApiPkgRev, true, nil
 	}
diff --git a/porch/pkg/registry/porch/packagerevision.go b/porch/pkg/registry/porch/packagerevision.go
index 1e74c63c..db22c3bb 100644
--- a/porch/pkg/registry/porch/packagerevision.go
+++ b/porch/pkg/registry/porch/packagerevision.go
@@ -77,7 +77,10 @@ func (r *packageRevisions) List(ctx context.Context, options *metainternalversio
 	}
 
 	if err := r.packageCommon.listPackageRevisions(ctx, filter, options.LabelSelector, func(p *engine.PackageRevision) error {
-		item := p.GetPackageRevision()
+		item, err := p.GetPackageRevision(ctx)
+		if err != nil {
+			return err
+		}
 		result.Items = append(result.Items, *item)
 		return nil
 	}); err != nil {
@@ -97,7 +100,10 @@ func (r *packageRevisions) Get(ctx context.Context, name string, options *metav1
 		return nil, err
 	}
 
-	apiPkgRev := repoPkgRev.GetPackageRevision()
+	apiPkgRev, err := repoPkgRev.GetPackageRevision(ctx)
+	if err != nil {
+		return nil, err
+	}
 
 	return apiPkgRev, nil
 }
@@ -144,7 +150,10 @@ func (r *packageRevisions) Create(ctx context.Context, runtimeObject runtime.Obj
 		return nil, apierrors.NewInternalError(err)
 	}
 
-	createdApiPkgRev := createdRepoPkgRev.GetPackageRevision()
+	createdApiPkgRev, err := createdRepoPkgRev.GetPackageRevision(ctx)
+	if err != nil {
+		return nil, apierrors.NewInternalError(err)
+	}
 
 	return createdApiPkgRev, nil
 }
@@ -186,7 +195,10 @@ func (r *packageRevisions) Delete(ctx context.Context, name string, deleteValida
 		return nil, false, err
 	}
 
-	apiPkgRev := repoPkgRev.GetPackageRevision()
+	apiPkgRev, err := repoPkgRev.GetPackageRevision(ctx)
+	if err != nil {
+		return nil, false, apierrors.NewInternalError(err)
+	}
 
 	repositoryObj, err := r.packageCommon.validateDelete(ctx, deleteValidation, apiPkgRev, name, ns)
 	if err != nil {
diff --git a/porch/pkg/registry/porch/packagerevisions_approval.go b/porch/pkg/registry/porch/packagerevisions_approval.go
index 34ac41ab..af5b00fc 100644
--- a/porch/pkg/registry/porch/packagerevisions_approval.go
+++ b/porch/pkg/registry/porch/packagerevisions_approval.go
@@ -51,8 +51,7 @@ func (a *packageRevisionsApproval) Get(ctx context.Context, name string, options
 	if err != nil {
 		return nil, err
 	}
-	obj := pkg.GetPackageRevision()
-	return obj, nil
+	return pkg.GetPackageRevision(ctx)
 }
 
 // Update finds a resource in the storage and updates it. Some implementations
diff --git a/porch/pkg/repository/repository.go b/porch/pkg/repository/repository.go
index 77850ca1..1827e771 100644
--- a/porch/pkg/repository/repository.go
+++ b/porch/pkg/repository/repository.go
@@ -62,14 +62,17 @@ type PackageRevision interface {
 	Lifecycle() v1alpha1.PackageRevisionLifecycle
 
 	// GetPackageRevision returns the PackageRevision ("DRY") API representation of this package-revision
-	GetPackageRevision() *v1alpha1.PackageRevision
+	GetPackageRevision(context.Context) (*v1alpha1.PackageRevision, error)
 
 	// GetResources returns the PackageRevisionResources ("WET") API representation of this package-revision
 	// TODO: return PackageResources or filesystem abstraction?
-	GetResources(ctx context.Context) (*v1alpha1.PackageRevisionResources, error)
+	GetResources(context.Context) (*v1alpha1.PackageRevisionResources, error)
 
 	// GetUpstreamLock returns the kpt lock information.
-	GetUpstreamLock() (kptfile.Upstream, kptfile.UpstreamLock, error)
+	GetUpstreamLock(context.Context) (kptfile.Upstream, kptfile.UpstreamLock, error)
+
+	// GetKptfile returns the Kptfile for hte package
+	GetKptfile(context.Context) (kptfile.KptFile, error)
 
 	// GetLock returns the current revision's lock information.
 	// This will be the upstream info for downstream revisions.
diff --git a/porch/pkg/repository/util.go b/porch/pkg/repository/util.go
new file mode 100644
index 00000000..6c31f8e2
--- /dev/null
+++ b/porch/pkg/repository/util.go
@@ -0,0 +1,62 @@
+// 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 repository
+
+import (
+	"fmt"
+
+	kptfile "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1"
+	api "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1"
+)
+
+func ToApiReadinessGates(kf kptfile.KptFile) []api.ReadinessGate {
+	var readinessGates []api.ReadinessGate
+	if kf.Info != nil {
+		for _, rg := range kf.Info.ReadinessGates {
+			readinessGates = append(readinessGates, api.ReadinessGate{
+				ConditionType: rg.ConditionType,
+			})
+		}
+	}
+	return readinessGates
+}
+
+func ToApiConditions(kf kptfile.KptFile) []api.Condition {
+	var conditions []api.Condition
+	if kf.Status != nil && kf.Status.Conditions != nil {
+		for _, s := range kf.Status.Conditions {
+			conditions = append(conditions, api.Condition{
+				Type:    s.Type,
+				Status:  toApiConditionStatus(s.Status),
+				Reason:  s.Reason,
+				Message: s.Message,
+			})
+		}
+	}
+	return conditions
+}
+
+func toApiConditionStatus(s kptfile.ConditionStatus) api.ConditionStatus {
+	switch s {
+	case kptfile.ConditionTrue:
+		return api.ConditionTrue
+	case kptfile.ConditionFalse:
+		return api.ConditionFalse
+	case kptfile.ConditionUnknown:
+		return api.ConditionUnknown
+	default:
+		panic(fmt.Errorf("unknown condition status: %v", s))
+	}
+}


Test patch:
diff --git a/commands/pkg/get/cmdget_test.go b/commands/pkg/get/cmdget_test.go
index 31afaa6d..a930caa5 100644
--- a/commands/pkg/get/cmdget_test.go
+++ b/commands/pkg/get/cmdget_test.go
@@ -66,8 +66,9 @@ func TestCmd_execute(t *testing.T) {
 				},
 			},
 			TypeMeta: yaml.TypeMeta{
-				APIVersion: kptfilev1.KptFileAPIVersion,
-				Kind:       kptfilev1.KptFileKind},
+				APIVersion: kptfilev1.KptFileGVK().GroupVersion().String(),
+				Kind:       kptfilev1.KptFileGVK().Kind,
+			},
 		},
 		Upstream: &kptfilev1.Upstream{
 			Type: kptfilev1.GitOrigin,
@@ -126,8 +127,9 @@ func TestCmdMainBranch_execute(t *testing.T) {
 				},
 			},
 			TypeMeta: yaml.TypeMeta{
-				APIVersion: kptfilev1.KptFileAPIVersion,
-				Kind:       kptfilev1.KptFileKind},
+				APIVersion: kptfilev1.KptFileGVK().GroupVersion().String(),
+				Kind:       kptfilev1.KptFileGVK().Kind,
+			},
 		},
 		Upstream: &kptfilev1.Upstream{
 			Type: kptfilev1.GitOrigin,
diff --git a/internal/util/fetch/fetch_test.go b/internal/util/fetch/fetch_test.go
index 0df81df2..c0c28174 100644
--- a/internal/util/fetch/fetch_test.go
+++ b/internal/util/fetch/fetch_test.go
@@ -196,8 +196,9 @@ func TestCommand_Run(t *testing.T) {
 				},
 			},
 			TypeMeta: yaml.TypeMeta{
-				APIVersion: kptfilev1.KptFileAPIVersion,
-				Kind:       kptfilev1.KptFileKind},
+				APIVersion: kptfilev1.KptFileGVK().GroupVersion().String(),
+				Kind:       kptfilev1.KptFileGVK().Kind,
+			},
 		},
 		Upstream: &kptfilev1.Upstream{
 			Type: "git",
@@ -258,8 +259,8 @@ func TestCommand_Run_subdir(t *testing.T) {
 				},
 			},
 			TypeMeta: yaml.TypeMeta{
-				APIVersion: kptfilev1.KptFileAPIVersion,
-				Kind:       kptfilev1.KptFileKind},
+				APIVersion: kptfilev1.KptFileGVK().GroupVersion().String(),
+				Kind:       kptfilev1.KptFileGVK().Kind},
 		},
 		Upstream: &kptfilev1.Upstream{
 			Type: kptfilev1.GitOrigin,
@@ -334,8 +335,9 @@ func TestCommand_Run_branch(t *testing.T) {
 				},
 			},
 			TypeMeta: yaml.TypeMeta{
-				APIVersion: kptfilev1.KptFileAPIVersion,
-				Kind:       kptfilev1.KptFileKind},
+				APIVersion: kptfilev1.KptFileGVK().GroupVersion().String(),
+				Kind:       kptfilev1.KptFileGVK().Kind,
+			},
 		},
 		Upstream: &kptfilev1.Upstream{
 			Type: kptfilev1.GitOrigin,
@@ -415,8 +417,9 @@ func TestCommand_Run_tag(t *testing.T) {
 				},
 			},
 			TypeMeta: yaml.TypeMeta{
-				APIVersion: kptfilev1.KptFileAPIVersion,
-				Kind:       kptfilev1.KptFileKind},
+				APIVersion: kptfilev1.KptFileGVK().GroupVersion().String(),
+				Kind:       kptfilev1.KptFileGVK().Kind,
+			},
 		},
 		Upstream: &kptfilev1.Upstream{
 			Type: "git",
diff --git a/pkg/kptfile/kptfileutil/util_test.go b/pkg/kptfile/kptfileutil/util_test.go
index 0dd153c6..9c0e21d8 100644
--- a/pkg/kptfile/kptfileutil/util_test.go
+++ b/pkg/kptfile/kptfileutil/util_test.go
@@ -321,6 +321,254 @@ kind: Kptfile
 metadata:
   name: foo
 pipeline: {}
+`,
+		},
+		"first readinessGate and condition added in upstream": {
+			origin: `
+apiVersion: kpt.dev/v1
+kind: Kptfile
+metadata:
+  name: foo
+`,
+			updated: `
+apiVersion: kpt.dev/v1
+kind: Kptfile
+metadata:
+  name: foo
+info:
+  readinessGates:
+  - conditionType: foo
+status:
+  conditions:
+  - type: foo
+    status: "True"
+    reason: reason
+    message: message
+`,
+			local: `
+apiVersion: kpt.dev/v1
+kind: Kptfile
+metadata:
+  name: foo
+`,
+			updateUpstream: false,
+			expected: `
+apiVersion: kpt.dev/v1
+kind: Kptfile
+metadata:
+  name: foo
+info:
+  readinessGates:
+    - conditionType: foo
+status:
+  conditions:
+    - type: foo
+      status: "True"
+      reason: reason
+      message: message
+`,
+		},
+		"additional readinessGate and condition added in upstream": {
+			origin: `
+apiVersion: kpt.dev/v1
+kind: Kptfile
+metadata:
+  name: foo
+info:
+  readinessGates:
+    - conditionType: foo
+status:
+  conditions:
+    - type: foo
+      status: "True"
+      reason: reason
+      message: message
+`,
+			updated: `
+apiVersion: kpt.dev/v1
+kind: Kptfile
+metadata:
+  name: foo
+info:
+  readinessGates:
+    - conditionType: foo
+    - conditionType: bar
+status:
+  conditions:
+    - type: foo
+      status: "True"
+      reason: reason
+      message: message
+    - type: bar
+      status: "False"
+      reason: reason
+      message: message
+`,
+			local: `
+apiVersion: kpt.dev/v1
+kind: Kptfile
+metadata:
+  name: foo
+info:
+  readinessGates:
+    - conditionType: foo
+status:
+  conditions:
+    - type: foo
+      status: "True"
+      reason: reason
+      message: message
+`,
+			updateUpstream: false,
+			expected: `
+apiVersion: kpt.dev/v1
+kind: Kptfile
+metadata:
+  name: foo
+info:
+  readinessGates:
+    - conditionType: foo
+    - conditionType: bar
+status:
+  conditions:
+    - type: foo
+      status: "True"
+      reason: reason
+      message: message
+    - type: bar
+      status: "False"
+      reason: reason
+      message: message
+		`,
+		},
+		"readinessGate added removed in upstream": {
+			origin: `
+apiVersion: kpt.dev/v1
+kind: Kptfile
+metadata:
+  name: foo
+info:
+  readinessGates:
+    - conditionType: foo
+status:
+  conditions:
+    - type: foo
+      status: "True"
+      reason: reason
+      message: message
+`,
+			updated: `
+apiVersion: kpt.dev/v1
+kind: Kptfile
+metadata:
+  name: foo
+`,
+			local: `
+apiVersion: kpt.dev/v1
+kind: Kptfile
+metadata:
+  name: foo
+info:
+  readinessGates:
+    - conditionType: foo
+status:
+  conditions:
+    - type: foo
+      status: "True"
+      reason: reason
+      message: message
+`,
+			updateUpstream: false,
+			expected: `
+apiVersion: kpt.dev/v1
+kind: Kptfile
+metadata:
+  name: foo
+info: {}
+status: {}
+`,
+		},
+		"readinessGates removed and added in both upstream and local": {
+			origin: `
+apiVersion: kpt.dev/v1
+kind: Kptfile
+metadata:
+  name: foo
+info:
+  readinessGates:
+    - conditionType: foo
+    - conditionType: bar
+status:
+  conditions:
+    - type: foo
+      status: "True"
+      reason: reason
+      message: message
+    - type: bar
+      status: "False"
+      reason: reason
+      message: message
+`,
+			updated: `
+apiVersion: kpt.dev/v1
+kind: Kptfile
+metadata:
+  name: foo
+info:
+  readinessGates:
+    - conditionType: foo
+    - conditionType: zork
+status:
+  conditions:
+    - type: foo
+      status: "True"
+      reason: reason
+      message: message
+    - type: zork
+      status: "Unknown"
+      reason: reason
+      message: message
+`,
+			local: `
+apiVersion: kpt.dev/v1
+kind: Kptfile
+metadata:
+  name: foo
+info:
+  readinessGates:
+    - conditionType: xandar
+    - conditionType: foo
+status:
+  conditions:
+    - type: xandar
+      status: "True"
+      reason: reason
+      message: message
+    - type: foo
+      status: "True"
+      reason: reason
+      message: message  
+`,
+			updateUpstream: false,
+			expected: `
+apiVersion: kpt.dev/v1
+kind: Kptfile
+metadata:
+  name: foo
+info:
+  readinessGates:
+    - conditionType: foo
+    - conditionType: zork
+status:
+  conditions:
+    - type: foo
+      status: "True"
+      reason: reason
+      message: message
+    - type: zork
+      status: Unknown
+      reason: reason
+      message: message
 `,
 		},
 	}
diff --git a/porch/pkg/cache/cache_test.go b/porch/pkg/cache/cache_test.go
index fbe41873..1933c687 100644
--- a/porch/pkg/cache/cache_test.go
+++ b/porch/pkg/cache/cache_test.go
@@ -53,7 +53,10 @@ func TestLatestPackages(t *testing.T) {
 
 	gotLatest := map[string]string{}
 	for _, pr := range revisions {
-		rev := pr.GetPackageRevision()
+		rev, err := pr.GetPackageRevision(ctx)
+		if err != nil {
+			t.Errorf("didn't expect error, but got %v", err)
+		}
 
 		if latest, ok := rev.Labels[api.LatestPackageRevisionKey]; ok {
 			if got, want := latest, api.LatestPackageRevisionValue; got != want {
@@ -111,7 +114,10 @@ func TestPublishedLatest(t *testing.T) {
 	if err != nil {
 		t.Fatalf("Close failed: %v", err)
 	}
-	resource := closed.GetPackageRevision()
+	resource, err := closed.GetPackageRevision(ctx)
+	if err != nil {
+		t.Errorf("didn't expect error, but got %v", err)
+	}
 	if got, ok := resource.Labels[api.LatestPackageRevisionKey]; !ok {
 		t.Errorf("Label %s not found as expected", api.LatestPackageRevisionKey)
 	} else if want := api.LatestPackageRevisionValue; got != want {
diff --git a/porch/pkg/engine/engine_test.go b/porch/pkg/engine/engine_test.go
new file mode 100644
index 00000000..d40cc153
--- /dev/null
+++ b/porch/pkg/engine/engine_test.go
@@ -0,0 +1,317 @@
+// 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 engine
+
+import (
+	"context"
+	"strings"
+	"testing"
+
+	kptfile "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1"
+	api "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1"
+	"github.com/GoogleContainerTools/kpt/porch/pkg/engine/fake"
+	"github.com/GoogleContainerTools/kpt/porch/pkg/repository"
+	"github.com/google/go-cmp/cmp"
+)
+
+func TestSomething(t *testing.T) {
+	testCases := map[string]struct {
+		repoPkgRev   repository.PackageRevision
+		newApiPkgRev *api.PackageRevision
+		hasPatch     bool
+		patch        api.PatchSpec
+	}{
+		"no gates or conditions": {
+			repoPkgRev: &fake.PackageRevision{
+				Kptfile: kptfile.KptFile{},
+			},
+			newApiPkgRev: &api.PackageRevision{
+				Spec: api.PackageRevisionSpec{},
+			},
+			hasPatch: false,
+		},
+		"first gate and condition added": {
+			repoPkgRev: &fake.PackageRevision{
+				Kptfile: kptfile.KptFile{},
+			},
+			newApiPkgRev: &api.PackageRevision{
+				Spec: api.PackageRevisionSpec{
+					ReadinessGates: []api.ReadinessGate{
+						{
+							ConditionType: "foo",
+						},
+					},
+				},
+				Status: api.PackageRevisionStatus{
+					Conditions: []api.Condition{
+						{
+							Type:   "foo",
+							Status: api.ConditionTrue,
+						},
+					},
+				},
+			},
+			hasPatch: true,
+			patch: api.PatchSpec{
+				File: kptfile.KptFileName,
+				Contents: strings.TrimSpace(`
+--- Kptfile
++++ Kptfile
+@@ -1 +1,7 @@
+-{}
++info:
++  readinessGates:
++  - conditionType: foo
++status:
++  conditions:
++  - type: foo
++    status: "True"				
+`) + "\n",
+				PatchType: api.PatchTypePatchFile,
+			},
+		},
+		"additional readinessGates and conditions added": {
+			repoPkgRev: &fake.PackageRevision{
+				Kptfile: kptfile.KptFile{
+					Info: &kptfile.PackageInfo{
+						ReadinessGates: []kptfile.ReadinessGate{
+							{
+								ConditionType: "foo",
+							},
+						},
+					},
+					Status: &kptfile.Status{
+						Conditions: []kptfile.Condition{
+							{
+								Type:   "foo",
+								Status: kptfile.ConditionTrue,
+							},
+						},
+					},
+				},
+			},
+			newApiPkgRev: &api.PackageRevision{
+				Spec: api.PackageRevisionSpec{
+					ReadinessGates: []api.ReadinessGate{
+						{
+							ConditionType: "foo",
+						},
+						{
+							ConditionType: "bar",
+						},
+					},
+				},
+				Status: api.PackageRevisionStatus{
+					Conditions: []api.Condition{
+						{
+							Type:    "foo",
+							Status:  api.ConditionTrue,
+							Reason:  "reason",
+							Message: "message",
+						},
+						{
+							Type:    "bar",
+							Status:  api.ConditionFalse,
+							Reason:  "reason",
+							Message: "message",
+						},
+					},
+				},
+			},
+			hasPatch: true,
+			patch: api.PatchSpec{
+				File: kptfile.KptFileName,
+				Contents: strings.TrimSpace(`
+--- Kptfile
++++ Kptfile
+@@ -1,7 +1,14 @@
+ info:
+   readinessGates:
+   - conditionType: foo
++  - conditionType: bar
+ status:
+   conditions:
+   - type: foo
+     status: "True"
++    reason: reason
++    message: message
++  - type: bar
++    status: "False"
++    reason: reason
++    message: message
+`) + "\n",
+				PatchType: api.PatchTypePatchFile,
+			},
+		},
+		"no changes": {
+			repoPkgRev: &fake.PackageRevision{
+				Kptfile: kptfile.KptFile{
+					Info: &kptfile.PackageInfo{
+						ReadinessGates: []kptfile.ReadinessGate{
+							{
+								ConditionType: "foo",
+							},
+							{
+								ConditionType: "bar",
+							},
+						},
+					},
+					Status: &kptfile.Status{
+						Conditions: []kptfile.Condition{
+							{
+								Type:    "foo",
+								Status:  kptfile.ConditionTrue,
+								Reason:  "reason",
+								Message: "message",
+							},
+							{
+								Type:    "bar",
+								Status:  kptfile.ConditionFalse,
+								Reason:  "reason",
+								Message: "message",
+							},
+						},
+					},
+				},
+			},
+			newApiPkgRev: &api.PackageRevision{
+				Spec: api.PackageRevisionSpec{
+					ReadinessGates: []api.ReadinessGate{
+						{
+							ConditionType: "foo",
+						},
+						{
+							ConditionType: "bar",
+						},
+					},
+				},
+				Status: api.PackageRevisionStatus{
+					Conditions: []api.Condition{
+						{
+							Type:    "foo",
+							Status:  api.ConditionTrue,
+							Reason:  "reason",
+							Message: "message",
+						},
+						{
+							Type:    "bar",
+							Status:  api.ConditionFalse,
+							Reason:  "reason",
+							Message: "message",
+						},
+					},
+				},
+			},
+			hasPatch: false,
+		},
+		"readinessGates and conditions removed": {
+			repoPkgRev: &fake.PackageRevision{
+				Kptfile: kptfile.KptFile{
+					Info: &kptfile.PackageInfo{
+						ReadinessGates: []kptfile.ReadinessGate{
+							{
+								ConditionType: "foo",
+							},
+							{
+								ConditionType: "bar",
+							},
+						},
+					},
+					Status: &kptfile.Status{
+						Conditions: []kptfile.Condition{
+							{
+								Type:    "foo",
+								Status:  kptfile.ConditionTrue,
+								Reason:  "reason",
+								Message: "message",
+							},
+							{
+								Type:    "bar",
+								Status:  kptfile.ConditionFalse,
+								Reason:  "reason",
+								Message: "message",
+							},
+						},
+					},
+				},
+			},
+			newApiPkgRev: &api.PackageRevision{
+				Spec: api.PackageRevisionSpec{
+					ReadinessGates: []api.ReadinessGate{
+						{
+							ConditionType: "foo",
+						},
+					},
+				},
+				Status: api.PackageRevisionStatus{
+					Conditions: []api.Condition{
+						{
+							Type:   "foo",
+							Status: api.ConditionTrue,
+						},
+					},
+				},
+			},
+			hasPatch: true,
+			patch: api.PatchSpec{
+				File: kptfile.KptFileName,
+				Contents: strings.TrimSpace(`
+--- Kptfile
++++ Kptfile
+@@ -1,14 +1,7 @@
+ info:
+   readinessGates:
+   - conditionType: foo
+-  - conditionType: bar
+ status:
+   conditions:
+   - type: foo
+     status: "True"
+-    reason: reason
+-    message: message
+-  - type: bar
+-    status: "False"
+-    reason: reason
+-    message: message
+`) + "\n",
+				PatchType: api.PatchTypePatchFile,
+			},
+		},
+	}
+
+	for tn := range testCases {
+		tc := testCases[tn]
+		t.Run(tn, func(t *testing.T) {
+			task, hasPatch, err := createKptfilePatchTask(context.Background(), tc.repoPkgRev, tc.newApiPkgRev)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			if tc.hasPatch && !hasPatch {
+				t.Errorf("expected patch, but didn't get one")
+			}
+			if !tc.hasPatch {
+				if hasPatch {
+					t.Errorf("expected no patch, but got one")
+				}
+				return
+			}
+
+			if diff := cmp.Diff(tc.patch, task.Patch.Patches[0]); diff != "" {
+				t.Errorf("Unexpected result (-want, +got): %s", diff)
+			}
+		})
+	}
+}
diff --git a/porch/pkg/git/git_test.go b/porch/pkg/git/git_test.go
index 7103b935..b8b4351c 100644
--- a/porch/pkg/git/git_test.go
+++ b/porch/pkg/git/git_test.go
@@ -419,7 +419,10 @@ func (g GitSuite) TestListPackagesTrivial(t *testing.T) {
 		t.Fatalf("draft.Close() failed: %v", err)
 	}
 
-	result := newRevision.GetPackageRevision()
+	result, err := newRevision.GetPackageRevision(ctx)
+	if err != nil {
+		t.Errorf("didn't expect error, but got %v", err)
+	}
 	if got, want := result.Spec.Lifecycle, v1alpha1.PackageRevisionLifecycleDraft; got != want {
 		t.Errorf("Newly created package type: got %q, want %q", got, want)
 	}
@@ -504,7 +507,10 @@ func (g GitSuite) TestCreatePackageInTrivialRepository(t *testing.T) {
 		t.Fatalf("draft.Close() failed: %v", err)
 	}
 
-	result := newRevision.GetPackageRevision()
+	result, err := newRevision.GetPackageRevision(ctx)
+	if err != nil {
+		t.Errorf("didn't expect error, but got %v", err)
+	}
 	if got, want := result.Spec.Lifecycle, v1alpha1.PackageRevisionLifecycleDraft; got != want {
 		t.Errorf("Newly created package type: got %q, want %q", got, want)
 	}
@@ -554,7 +560,10 @@ func (g GitSuite) TestListPackagesSimple(t *testing.T) {
 
 	got := map[repository.PackageRevisionKey]v1alpha1.PackageRevisionLifecycle{}
 	for _, r := range revisions {
-		rev := r.GetPackageRevision()
+		rev, err := r.GetPackageRevision(ctx)
+		if err != nil {
+			t.Errorf("didn't expect error, but got %v", err)
+		}
 		got[repository.PackageRevisionKey{
 			Repository: rev.Spec.RepositoryName,
 			Package:    rev.Spec.PackageName,
@@ -613,7 +622,10 @@ func (g GitSuite) TestListPackagesDrafts(t *testing.T) {
 
 	got := map[repository.PackageRevisionKey]v1alpha1.PackageRevisionLifecycle{}
 	for _, r := range revisions {
-		rev := r.GetPackageRevision()
+		rev, err := r.GetPackageRevision(ctx)
+		if err != nil {
+			t.Errorf("didn't expect error, but got %v", err)
+		}
 		got[repository.PackageRevisionKey{
 			Repository: rev.Spec.RepositoryName,
 			Package:    rev.Spec.PackageName,
@@ -675,7 +687,10 @@ func (g GitSuite) TestApproveDraft(t *testing.T) {
 		t.Fatalf("Close failed: %v", err)
 	}
 
-	rev := new.GetPackageRevision()
+	rev, err := new.GetPackageRevision(ctx)
+	if err != nil {
+		t.Errorf("didn't expect error, but got %v", err)
+	}
 	if got, want := rev.Spec.Lifecycle, v1alpha1.PackageRevisionLifecyclePublished; got != want {
 		t.Errorf("Approved package lifecycle: got %s, want %s", got, want)
 	}
@@ -734,7 +749,10 @@ func (g GitSuite) TestApproveDraftWithHistory(t *testing.T) {
 		t.Fatalf("Close failed: %v", err)
 	}
 
-	rev := new.GetPackageRevision()
+	rev, err := new.GetPackageRevision(ctx)
+	if err != nil {
+		t.Errorf("didn't expect error, but got %v", err)
+	}
 	if got, want := rev.Spec.Lifecycle, v1alpha1.PackageRevisionLifecyclePublished; got != want {
 		t.Errorf("Approved package lifecycle: got %s, want %s", got, want)
 	}
@@ -787,7 +805,10 @@ func (g GitSuite) TestDeletePackages(t *testing.T) {
 	for len(all) > 0 {
 		// Delete one of the packages
 		deleting := all[0]
-		pr := deleting.GetPackageRevision()
+		pr, err := deleting.GetPackageRevision(ctx)
+		if err != nil {
+			t.Fatalf("didn't expect error, but got %v", err)
+		}
 		name := repository.PackageRevisionKey{Repository: pr.Spec.RepositoryName, Package: pr.Spec.PackageName, Revision: pr.Spec.Revision}
 
 		if rn, ok := wantDeletedRefs[name]; ok {
@@ -1050,7 +1071,10 @@ func (g GitSuite) TestNested(t *testing.T) {
 
 	got := map[string]v1alpha1.PackageRevisionLifecycle{}
 	for _, pr := range revisions {
-		rev := pr.GetPackageRevision()
+		rev, err := pr.GetPackageRevision(ctx)
+		if err != nil {
+			t.Errorf("didn't expect error, but got %v", err)
+		}
 		if rev.Spec.Revision == g.branch {
 			// skip packages with the revision of the main registered branch,
 			// to match the above simplified package discovery algo.
@@ -1209,7 +1233,10 @@ func (g GitSuite) TestAuthor(t *testing.T) {
 				Package:    tc.pkg,
 				Revision:   tc.revision,
 			})
-			rev := draftPkg.GetPackageRevision()
+			rev, err := draftPkg.GetPackageRevision(ctx)
+			if err != nil {
+				t.Errorf("didn't expect error, but got %v", err)
+			}
 			if got, want := rev.Status.PublishedBy, tc.author; got != want {
 				t.Errorf("expected %q, but got %q", want, got)
 			}