Repository: kptdev/kpt

Issue:
Add 'package' resource
Currently the porch api doesn't expose a package resource. Instead, we expose package revisions with the grouping revisions that is part of the same package only available through the `PackageName` field on revisions.

We need to determine how the different operations on a package will translate into packagerevisions. For example, if a user mutates a package, presumably that would automatically create a new package revision. We also need to consider if we also need to add a packageresources that mirrors packagerevisionresources, but that always works on the latest revision.


Discussion:
@natasha41575: A bit of a clarification question, what exactly do we mean by 'package' resource? I'm imagining a resource that contains references to all of the package revisions that belong to the package, i.e. something like:

```
apiVersion: porch.kpt.dev/v1alpha1
kind: Package
metadata:
  namespace: {namespace}
  name: {repository-hashsuffix}
spec:
  repository: {repository}
  packageName: {package name}
status:
  revisions: 
  - {list of PackageRevision objects that belong to this Package}
  latestRevision: {name of the latest packageRevision}
```

Is this aligned with what you had in mind? If we want porch to automatically group together package revisions that have the same package name, then perhaps that information belongs in the status field?

With that mental model, I'm trying to think more about what you mean here:

> For example, if a user mutates a package, presumably that would automatically create a new package revision.

What kind of "mutations" are we anticipating that users will do on a Package resource? The only one that comes to mind for me are to add or remove PackageRevisions, or modify package resources. It seems like most mutations done to the Package object itself should be handled by porch, and not the user directly. Is there anything else to account for? 

@mortent: I'm hoping that we can define operations on the `package` resource that "papered over" the underlying package revisions. For example:
 * Creating a new package would mean creating the initial package revision.
 * Getting a package means getting the latest package revision.
 * Deleting a package means deleting the package and all package revisions.

I'm not yet sure if we can define sensible behavior for all the verbs. For example, does it make sense that getting a package returns the last package revision while deleting a package removes all package revisions? I think it seems ok, but I haven't thought enough about it.

I don't think we need to include the list of package revisions in the status. Users can get this information directly from the API by listing the package revisions.@johnbelamaric: I am not sure this is the right approach.  I can't think of other K8s resources off hand that have this sort of relationship. It makes sense to explore the use cases. But if what we are talking about is primarily bulk operations on top of revisions, appropriate labels may be sufficient (if we implement label selectors ala #3402 ).

I suspect we should move to CRDs for everything except the actual ResourceList content.@bgrant0607: @johnbelamaric Deployment and ReplicaSet. Job and Pods.@bgrant0607: Enumerating all the revisions in one resource is a scalability problem and violates Kubernetes API conventions. Anyone who wants to see the revisions can list the revisions separately.@johnbelamaric: > @johnbelamaric Deployment and ReplicaSet. Job and Pods.

I don't really think of those as "containment" relationships, which is how I see this. Deployments create and manage ReplicaSets, and the attributes of the Deployment control how that is done. Similar with Jobs and Pods.

I don't see a Package controller that is managing PackageRevision resources. I guess maybe that's a way to implement the tagging. What would a PackageSpec look like? Something "feels" off with this - but I can't put my finger on it without more thought. It feels like a sort of "psuedo" resource. Probably seeing the operations will help.@bgrant0607: Package would create and manage PackageRevisions. 

How we would implement deletion is an interesting question. There are multiple options, including asynchronous GC.

The whole reason for aggregated APIs in general was to allow functionality not present in the core K8s apiserver and/or to use different storage than the core etcd instance. The original use case was core metrics. The mechanism was also used to implement CRDs (https://github.com/kubernetes/apiextensions-apiserver). Here we're using it to use alternative storage also, to provide some data-plane-like operations, and to implement some functionality synchronously.

Another example of providing a veneer or virtual view on top of other resources is:
https://github.com/openshift/kube-projects
@natasha41575: Based on other discussion, I think the following makes sense:

1. Creating a new package will create an empty package with no package revisions or resources. Creating a package revision should automatically create a corresponding package object if one doesn't already exist. 

> Creating a new package would mean creating the initial package revision.

After discussion with @mortent and @justinsb, it seems like this might not be the best thing to allow as it would require the package to take parameters to tell it where the upstream package is, which could complicate the package resource. 

2. Getting a package means getting the most recent package revision.

3. Deletion of a package means deleting all the package revisions (so all the branches and tags from the git repository), and also removing the package from the `main` branch. 

IIUC, the package revision should look something like this: 

```
apiVersion: porch.kpt.dev/v1alpha1
kind: Package
metadata:
  namespace: {namespace}
  name: {repository-hashsuffix}
spec:
  repository: {repository}
  packageName: {package name}
status:
  latestRevision: {name of the latest packageRevision}
```

and, as Morten described above, we should have a `packageResources` that mirrors `packageRevisionResources`.

I'm planning to take an initial stab at implementing this, but welcome more discussion and feedback about what the verbs should mean and what the resource should look like. 

Edited to add:

For having deletion go through an approval process, we can do the same thing that we currently do with PackageRevisions, using subresources. 

@bgrant0607: On creating a new package:

It's a good point that we have at least two ways of creating new packages currently, init and get:

https://github.com/GoogleContainerTools/kpt/blob/main/docs/design-docs/07-package-orchestration.md#package-authoring@liamfallon: Duplicate of https://github.com/nephio-project/nephio/issues/648

Solution patch:
diff --git a/internal/cmdrender/cmdrender.go b/internal/cmdrender/cmdrender.go
index 615bcadc..a7822fbe 100644
--- a/internal/cmdrender/cmdrender.go
+++ b/internal/cmdrender/cmdrender.go
@@ -36,6 +36,8 @@ import (
 // NewRunner returns a command runner
 func NewRunner(ctx context.Context, parent string) *Runner {
 	r := &Runner{ctx: ctx}
+	r.InitDefaults()
+
 	c := &cobra.Command{
 		Use:     "render [PKG_PATH] [flags]",
 		Short:   docs.RenderShort,
@@ -48,8 +50,13 @@ func NewRunner(ctx context.Context, parent string) *Runner {
 		"path to a directory to save function results")
 	c.Flags().StringVarP(&r.dest, "output", "o", "",
 		fmt.Sprintf("output resources are written to provided location. Allowed values: %s|%s|<OUT_DIR_PATH>", cmdutil.Stdout, cmdutil.Unwrap))
-	c.Flags().StringVar(&r.imagePullPolicy, "image-pull-policy", string(fnruntime.IfNotPresentPull),
-		fmt.Sprintf("pull image before running the container. It must be one of %s, %s and %s.", fnruntime.AlwaysPull, fnruntime.IfNotPresentPull, fnruntime.NeverPull))
+
+	c.Flags().Var(&r.imagePullPolicy, "image-pull-policy",
+		"pull image before running the container "+r.imagePullPolicy.HelpAllowedValues())
+	_ = c.RegisterFlagCompletionFunc("image-pull-policy", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+		return r.imagePullPolicy.AllStrings(), cobra.ShellCompDirectiveDefault
+	})
+
 	c.Flags().BoolVar(&r.allowExec, "allow-exec", false,
 		"allow binary executable to be run during pipeline execution.")
 	c.Flags().BoolVar(&r.allowWasm, "allow-alpha-wasm", false,
@@ -67,7 +74,7 @@ func NewCommand(ctx context.Context, parent string) *cobra.Command {
 type Runner struct {
 	pkgPath         string
 	resultsDirPath  string
-	imagePullPolicy string
+	imagePullPolicy fnruntime.ImagePullPolicy
 	allowExec       bool
 	allowWasm       bool
 	dest            string
@@ -75,6 +82,10 @@ type Runner struct {
 	ctx             context.Context
 }
 
+func (r *Runner) InitDefaults() {
+	r.imagePullPolicy = fnruntime.IfNotPresentPull
+}
+
 func (r *Runner) preRunE(c *cobra.Command, args []string) error {
 	if len(args) == 0 {
 		// no pkg path specified, default to current working dir
@@ -103,7 +114,7 @@ func (r *Runner) preRunE(c *cobra.Command, args []string) error {
 			return fmt.Errorf("cannot read or create results dir %q: %w", r.resultsDirPath, err)
 		}
 	}
-	return cmdutil.ValidateImagePullPolicyValue(r.imagePullPolicy)
+	return nil
 }
 
 func (r *Runner) runE(c *cobra.Command, _ []string) error {
@@ -122,7 +133,7 @@ func (r *Runner) runE(c *cobra.Command, _ []string) error {
 		PkgPath:         absPkgPath,
 		ResultsDirPath:  r.resultsDirPath,
 		Output:          output,
-		ImagePullPolicy: cmdutil.StringToImagePullPolicy(r.imagePullPolicy),
+		ImagePullPolicy: r.imagePullPolicy,
 		AllowExec:       r.allowExec,
 		AllowWasm:       r.allowWasm,
 		FileSystem:      filesys.FileSystemOrOnDisk{},
diff --git a/internal/fnruntime/container.go b/internal/fnruntime/container.go
index cec22b99..a04995b9 100644
--- a/internal/fnruntime/container.go
+++ b/internal/fnruntime/container.go
@@ -54,10 +54,6 @@ const (
 	podmanBin  string = "podman"
 	nerdctlBin string = "nerdctl"
 
-	AlwaysPull       ImagePullPolicy = "Always"
-	IfNotPresentPull ImagePullPolicy = "IfNotPresent"
-	NeverPull        ImagePullPolicy = "Never"
-
 	ContainerRuntimeEnv = "KPT_FN_RUNTIME"
 
 	Docker  ContainerRuntime = "docker"
@@ -65,8 +61,6 @@ const (
 	Nerdctl ContainerRuntime = "nerdctl"
 )
 
-type ImagePullPolicy string
-
 type ContainerRuntime string
 
 // ContainerFnPermission contains the permission of container
@@ -285,17 +279,17 @@ func filterDockerCLIOutput(in io.Reader) string {
 // isdockerCLIoutput is helper method to determine if
 // the given string is a docker CLI output message.
 // Example docker output:
-//	"Unable to find image 'gcr.io/kpt-fn/starlark:v0.3' locally"
-//  "v0.3: Pulling from kpt-fn/starlark"
-//  "4e9f2cdf4387: Already exists"
-//  "aafbf7df3ddf: Pulling fs layer"
-//  "aafbf7df3ddf: Verifying Checksum"
-//  "aafbf7df3ddf: Download complete"
-//  "6b759ab96cb2: Waiting"
-//  "aafbf7df3ddf: Pull complete"
-//  "Digest: sha256:c347e28606fa1a608e8e02e03541a5a46e4a0152005df4a11e44f6c4ab1edd9a"
-//  "Status: Downloaded newer image for gcr.io/kpt-fn/starlark:v0.3"
 //
+//	"Unable to find image 'gcr.io/kpt-fn/starlark:v0.3' locally"
+//	"v0.3: Pulling from kpt-fn/starlark"
+//	"4e9f2cdf4387: Already exists"
+//	"aafbf7df3ddf: Pulling fs layer"
+//	"aafbf7df3ddf: Verifying Checksum"
+//	"aafbf7df3ddf: Download complete"
+//	"6b759ab96cb2: Waiting"
+//	"aafbf7df3ddf: Pull complete"
+//	"Digest: sha256:c347e28606fa1a608e8e02e03541a5a46e4a0152005df4a11e44f6c4ab1edd9a"
+//	"Status: Downloaded newer image for gcr.io/kpt-fn/starlark:v0.3"
 func isdockerCLIoutput(s string) bool {
 	if strings.Contains(s, ": Already exists") ||
 		strings.Contains(s, ": Pulling fs layer") ||
@@ -332,14 +326,14 @@ var sha256Matcher = regexp.MustCompile(`^[A-Fa-f0-9]{64}$`)
 // isPodmanCLIoutput is helper method to determine if
 // the given string is a podman CLI output message.
 // Example podman output:
-//  "Trying to pull gcr.io/kpt-fn/starlark:v0.3..."
-//  "Getting image source signatures"
-//  "Copying blob sha256:aafbf7df3ddf625f4ababc8e55b4a09131651f9aac340b852b5f40b1a53deb65"
-//  "Copying config sha256:17ce4f65660717ba0afbd143578dfd1c5b9822bd3ad3945c10d6878e057265f1"
-//  "Writing manifest to image destination"
-//  "Storing signatures"
-//  "17ce4f65660717ba0afbd143578dfd1c5b9822bd3ad3945c10d6878e057265f1"
 //
+//	"Trying to pull gcr.io/kpt-fn/starlark:v0.3..."
+//	"Getting image source signatures"
+//	"Copying blob sha256:aafbf7df3ddf625f4ababc8e55b4a09131651f9aac340b852b5f40b1a53deb65"
+//	"Copying config sha256:17ce4f65660717ba0afbd143578dfd1c5b9822bd3ad3945c10d6878e057265f1"
+//	"Writing manifest to image destination"
+//	"Storing signatures"
+//	"17ce4f65660717ba0afbd143578dfd1c5b9822bd3ad3945c10d6878e057265f1"
 func isPodmanCLIoutput(s string) bool {
 	if strings.Contains(s, "Trying to pull") ||
 		strings.Contains(s, "Getting image source signatures") ||
diff --git a/internal/fnruntime/imagepullpolicy.go b/internal/fnruntime/imagepullpolicy.go
new file mode 100644
index 00000000..1e87714f
--- /dev/null
+++ b/internal/fnruntime/imagepullpolicy.go
@@ -0,0 +1,75 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package fnruntime
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/spf13/pflag"
+)
+
+type ImagePullPolicy string
+
+const (
+	AlwaysPull       ImagePullPolicy = "Always"
+	IfNotPresentPull ImagePullPolicy = "IfNotPresent"
+	NeverPull        ImagePullPolicy = "Never"
+)
+
+var allImagePullPolicy = []ImagePullPolicy{
+	AlwaysPull,
+	IfNotPresentPull,
+	NeverPull,
+}
+
+// ImagePullPolicy can be used in pflag
+var _ pflag.Value = ((*ImagePullPolicy)(nil))
+
+// String implements pflag.Value and fmt.Stringer
+func (e *ImagePullPolicy) String() string {
+	return string(*e)
+}
+
+// Set implements pflag.Value
+func (e *ImagePullPolicy) Set(v string) error {
+	l := strings.ToLower(v)
+	for _, c := range allImagePullPolicy {
+		s := strings.ToLower(string(c))
+		if s == l {
+			*e = c
+			return nil
+		}
+	}
+	return fmt.Errorf("must must be one of " + strings.Join(e.AllStrings(), ", "))
+}
+
+func (e *ImagePullPolicy) AllStrings() []string {
+	var allStrings []string
+	for _, c := range allImagePullPolicy {
+		allStrings = append(allStrings, string(c))
+	}
+	return allStrings
+}
+
+// HelpAllowedValues builds help text for the allowed values
+func (e *ImagePullPolicy) HelpAllowedValues() string {
+	return "(one of " + strings.Join(e.AllStrings(), ", ") + ")"
+}
+
+// Type implements pflag.Value
+func (e *ImagePullPolicy) Type() string {
+	return "ImagePullPolicy"
+}
diff --git a/internal/util/cmdutil/cmdutil.go b/internal/util/cmdutil/cmdutil.go
index 188ad24f..578cd9dd 100644
--- a/internal/util/cmdutil/cmdutil.go
+++ b/internal/util/cmdutil/cmdutil.go
@@ -21,7 +21,6 @@ import (
 	"os"
 	"strings"
 
-	"github.com/GoogleContainerTools/kpt/internal/fnruntime"
 	"github.com/GoogleContainerTools/kpt/internal/util/function"
 	"github.com/GoogleContainerTools/kpt/internal/util/httputil"
 	"github.com/GoogleContainerTools/kpt/internal/util/porch"
@@ -59,27 +58,6 @@ func PrintErrorStacktrace() bool {
 // StackOnError if true, will print a stack trace on failure.
 var StackOnError bool
 
-func ValidateImagePullPolicyValue(v string) error {
-	v = strings.ToLower(v)
-	if v != strings.ToLower(string(fnruntime.AlwaysPull)) &&
-		v != strings.ToLower(string(fnruntime.IfNotPresentPull)) &&
-		v != strings.ToLower(string(fnruntime.NeverPull)) {
-		return fmt.Errorf("image pull policy must be one of %s, %s and %s", fnruntime.AlwaysPull, fnruntime.IfNotPresentPull, fnruntime.NeverPull)
-	}
-	return nil
-}
-
-func StringToImagePullPolicy(v string) fnruntime.ImagePullPolicy {
-	switch strings.ToLower(v) {
-	case strings.ToLower(string(fnruntime.NeverPull)):
-		return fnruntime.NeverPull
-	case strings.ToLower(string(fnruntime.IfNotPresentPull)):
-		return fnruntime.IfNotPresentPull
-	default:
-		return fnruntime.AlwaysPull
-	}
-}
-
 // WriteFnOutput writes the output resources of function commands to provided destination
 func WriteFnOutput(dest, content string, fromStdin bool, w io.Writer) error {
 	r := strings.NewReader(content)
diff --git a/porch/api/generated/clientset/versioned/typed/porch/v1alpha1/fake/fake_pkg.go b/porch/api/generated/clientset/versioned/typed/porch/v1alpha1/fake/fake_pkg.go
new file mode 100644
index 00000000..30c25311
--- /dev/null
+++ b/porch/api/generated/clientset/versioned/typed/porch/v1alpha1/fake/fake_pkg.go
@@ -0,0 +1,140 @@
+// 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.
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package fake
+
+import (
+	"context"
+
+	v1alpha1 "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	labels "k8s.io/apimachinery/pkg/labels"
+	schema "k8s.io/apimachinery/pkg/runtime/schema"
+	types "k8s.io/apimachinery/pkg/types"
+	watch "k8s.io/apimachinery/pkg/watch"
+	testing "k8s.io/client-go/testing"
+)
+
+// FakePackages implements PackageInterface
+type FakePackages struct {
+	Fake *FakePorchV1alpha1
+	ns   string
+}
+
+var packagesResource = schema.GroupVersionResource{Group: "porch.kpt.dev", Version: "v1alpha1", Resource: "packages"}
+
+var packagesKind = schema.GroupVersionKind{Group: "porch.kpt.dev", Version: "v1alpha1", Kind: "Package"}
+
+// Get takes name of the pkg, and returns the corresponding pkg object, and an error if there is any.
+func (c *FakePackages) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.Package, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewGetAction(packagesResource, c.ns, name), &v1alpha1.Package{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.Package), err
+}
+
+// List takes label and field selectors, and returns the list of Packages that match those selectors.
+func (c *FakePackages) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.PackageList, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewListAction(packagesResource, packagesKind, c.ns, opts), &v1alpha1.PackageList{})
+
+	if obj == nil {
+		return nil, err
+	}
+
+	label, _, _ := testing.ExtractFromListOptions(opts)
+	if label == nil {
+		label = labels.Everything()
+	}
+	list := &v1alpha1.PackageList{ListMeta: obj.(*v1alpha1.PackageList).ListMeta}
+	for _, item := range obj.(*v1alpha1.PackageList).Items {
+		if label.Matches(labels.Set(item.Labels)) {
+			list.Items = append(list.Items, item)
+		}
+	}
+	return list, err
+}
+
+// Watch returns a watch.Interface that watches the requested packages.
+func (c *FakePackages) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
+	return c.Fake.
+		InvokesWatch(testing.NewWatchAction(packagesResource, c.ns, opts))
+
+}
+
+// Create takes the representation of a pkg and creates it.  Returns the server's representation of the pkg, and an error, if there is any.
+func (c *FakePackages) Create(ctx context.Context, pkg *v1alpha1.Package, opts v1.CreateOptions) (result *v1alpha1.Package, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewCreateAction(packagesResource, c.ns, pkg), &v1alpha1.Package{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.Package), err
+}
+
+// Update takes the representation of a pkg and updates it. Returns the server's representation of the pkg, and an error, if there is any.
+func (c *FakePackages) Update(ctx context.Context, pkg *v1alpha1.Package, opts v1.UpdateOptions) (result *v1alpha1.Package, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewUpdateAction(packagesResource, c.ns, pkg), &v1alpha1.Package{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.Package), err
+}
+
+// UpdateStatus was generated because the type contains a Status member.
+// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus().
+func (c *FakePackages) UpdateStatus(ctx context.Context, pkg *v1alpha1.Package, opts v1.UpdateOptions) (*v1alpha1.Package, error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewUpdateSubresourceAction(packagesResource, "status", c.ns, pkg), &v1alpha1.Package{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.Package), err
+}
+
+// Delete takes name of the pkg and deletes it. Returns an error if one occurs.
+func (c *FakePackages) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
+	_, err := c.Fake.
+		Invokes(testing.NewDeleteActionWithOptions(packagesResource, c.ns, name, opts), &v1alpha1.Package{})
+
+	return err
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *FakePackages) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
+	action := testing.NewDeleteCollectionAction(packagesResource, c.ns, listOpts)
+
+	_, err := c.Fake.Invokes(action, &v1alpha1.PackageList{})
+	return err
+}
+
+// Patch applies the patch and returns the patched pkg.
+func (c *FakePackages) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.Package, err error) {
+	obj, err := c.Fake.
+		Invokes(testing.NewPatchSubresourceAction(packagesResource, c.ns, name, pt, data, subresources...), &v1alpha1.Package{})
+
+	if obj == nil {
+		return nil, err
+	}
+	return obj.(*v1alpha1.Package), err
+}
diff --git a/porch/api/generated/clientset/versioned/typed/porch/v1alpha1/fake/fake_porch_client.go b/porch/api/generated/clientset/versioned/typed/porch/v1alpha1/fake/fake_porch_client.go
index 9a4ce183..7e951350 100644
--- a/porch/api/generated/clientset/versioned/typed/porch/v1alpha1/fake/fake_porch_client.go
+++ b/porch/api/generated/clientset/versioned/typed/porch/v1alpha1/fake/fake_porch_client.go
@@ -30,6 +30,10 @@ func (c *FakePorchV1alpha1) Functions(namespace string) v1alpha1.FunctionInterfa
 	return &FakeFunctions{c, namespace}
 }
 
+func (c *FakePorchV1alpha1) Packages(namespace string) v1alpha1.PackageInterface {
+	return &FakePackages{c, namespace}
+}
+
 func (c *FakePorchV1alpha1) PackageRevisions(namespace string) v1alpha1.PackageRevisionInterface {
 	return &FakePackageRevisions{c, namespace}
 }
diff --git a/porch/api/generated/clientset/versioned/typed/porch/v1alpha1/generated_expansion.go b/porch/api/generated/clientset/versioned/typed/porch/v1alpha1/generated_expansion.go
index 5db52576..24b4b473 100644
--- a/porch/api/generated/clientset/versioned/typed/porch/v1alpha1/generated_expansion.go
+++ b/porch/api/generated/clientset/versioned/typed/porch/v1alpha1/generated_expansion.go
@@ -18,6 +18,8 @@ package v1alpha1
 
 type FunctionExpansion interface{}
 
+type PackageExpansion interface{}
+
 type PackageRevisionExpansion interface{}
 
 type PackageRevisionResourcesExpansion interface{}
diff --git a/porch/api/generated/clientset/versioned/typed/porch/v1alpha1/pkg.go b/porch/api/generated/clientset/versioned/typed/porch/v1alpha1/pkg.go
new file mode 100644
index 00000000..5a4b105b
--- /dev/null
+++ b/porch/api/generated/clientset/versioned/typed/porch/v1alpha1/pkg.go
@@ -0,0 +1,193 @@
+// 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.
+
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	"context"
+	"time"
+
+	scheme "github.com/GoogleContainerTools/kpt/porch/api/generated/clientset/versioned/scheme"
+	v1alpha1 "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	types "k8s.io/apimachinery/pkg/types"
+	watch "k8s.io/apimachinery/pkg/watch"
+	rest "k8s.io/client-go/rest"
+)
+
+// PackagesGetter has a method to return a PackageInterface.
+// A group's client should implement this interface.
+type PackagesGetter interface {
+	Packages(namespace string) PackageInterface
+}
+
+// PackageInterface has methods to work with Package resources.
+type PackageInterface interface {
+	Create(ctx context.Context, pkg *v1alpha1.Package, opts v1.CreateOptions) (*v1alpha1.Package, error)
+	Update(ctx context.Context, pkg *v1alpha1.Package, opts v1.UpdateOptions) (*v1alpha1.Package, error)
+	UpdateStatus(ctx context.Context, pkg *v1alpha1.Package, opts v1.UpdateOptions) (*v1alpha1.Package, error)
+	Delete(ctx context.Context, name string, opts v1.DeleteOptions) error
+	DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error
+	Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.Package, error)
+	List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.PackageList, error)
+	Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error)
+	Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.Package, err error)
+	PackageExpansion
+}
+
+// packages implements PackageInterface
+type packages struct {
+	client rest.Interface
+	ns     string
+}
+
+// newPackages returns a Packages
+func newPackages(c *PorchV1alpha1Client, namespace string) *packages {
+	return &packages{
+		client: c.RESTClient(),
+		ns:     namespace,
+	}
+}
+
+// Get takes name of the pkg, and returns the corresponding pkg object, and an error if there is any.
+func (c *packages) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1alpha1.Package, err error) {
+	result = &v1alpha1.Package{}
+	err = c.client.Get().
+		Namespace(c.ns).
+		Resource("packages").
+		Name(name).
+		VersionedParams(&options, scheme.ParameterCodec).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// List takes label and field selectors, and returns the list of Packages that match those selectors.
+func (c *packages) List(ctx context.Context, opts v1.ListOptions) (result *v1alpha1.PackageList, err error) {
+	var timeout time.Duration
+	if opts.TimeoutSeconds != nil {
+		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
+	}
+	result = &v1alpha1.PackageList{}
+	err = c.client.Get().
+		Namespace(c.ns).
+		Resource("packages").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Watch returns a watch.Interface that watches the requested packages.
+func (c *packages) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
+	var timeout time.Duration
+	if opts.TimeoutSeconds != nil {
+		timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
+	}
+	opts.Watch = true
+	return c.client.Get().
+		Namespace(c.ns).
+		Resource("packages").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Watch(ctx)
+}
+
+// Create takes the representation of a pkg and creates it.  Returns the server's representation of the pkg, and an error, if there is any.
+func (c *packages) Create(ctx context.Context, pkg *v1alpha1.Package, opts v1.CreateOptions) (result *v1alpha1.Package, err error) {
+	result = &v1alpha1.Package{}
+	err = c.client.Post().
+		Namespace(c.ns).
+		Resource("packages").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(pkg).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Update takes the representation of a pkg and updates it. Returns the server's representation of the pkg, and an error, if there is any.
+func (c *packages) Update(ctx context.Context, pkg *v1alpha1.Package, opts v1.UpdateOptions) (result *v1alpha1.Package, err error) {
+	result = &v1alpha1.Package{}
+	err = c.client.Put().
+		Namespace(c.ns).
+		Resource("packages").
+		Name(pkg.Name).
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(pkg).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// UpdateStatus was generated because the type contains a Status member.
+// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus().
+func (c *packages) UpdateStatus(ctx context.Context, pkg *v1alpha1.Package, opts v1.UpdateOptions) (result *v1alpha1.Package, err error) {
+	result = &v1alpha1.Package{}
+	err = c.client.Put().
+		Namespace(c.ns).
+		Resource("packages").
+		Name(pkg.Name).
+		SubResource("status").
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(pkg).
+		Do(ctx).
+		Into(result)
+	return
+}
+
+// Delete takes name of the pkg and deletes it. Returns an error if one occurs.
+func (c *packages) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error {
+	return c.client.Delete().
+		Namespace(c.ns).
+		Resource("packages").
+		Name(name).
+		Body(&opts).
+		Do(ctx).
+		Error()
+}
+
+// DeleteCollection deletes a collection of objects.
+func (c *packages) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error {
+	var timeout time.Duration
+	if listOpts.TimeoutSeconds != nil {
+		timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second
+	}
+	return c.client.Delete().
+		Namespace(c.ns).
+		Resource("packages").
+		VersionedParams(&listOpts, scheme.ParameterCodec).
+		Timeout(timeout).
+		Body(&opts).
+		Do(ctx).
+		Error()
+}
+
+// Patch applies the patch and returns the patched pkg.
+func (c *packages) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.Package, err error) {
+	result = &v1alpha1.Package{}
+	err = c.client.Patch(pt).
+		Namespace(c.ns).
+		Resource("packages").
+		Name(name).
+		SubResource(subresources...).
+		VersionedParams(&opts, scheme.ParameterCodec).
+		Body(data).
+		Do(ctx).
+		Into(result)
+	return
+}
diff --git a/porch/api/generated/clientset/versioned/typed/porch/v1alpha1/porch_client.go b/porch/api/generated/clientset/versioned/typed/porch/v1alpha1/porch_client.go
index 83c0b4dc..e343685e 100644
--- a/porch/api/generated/clientset/versioned/typed/porch/v1alpha1/porch_client.go
+++ b/porch/api/generated/clientset/versioned/typed/porch/v1alpha1/porch_client.go
@@ -27,6 +27,7 @@ import (
 type PorchV1alpha1Interface interface {
 	RESTClient() rest.Interface
 	FunctionsGetter
+	PackagesGetter
 	PackageRevisionsGetter
 	PackageRevisionResourcesGetter
 }
@@ -40,6 +41,10 @@ func (c *PorchV1alpha1Client) Functions(namespace string) FunctionInterface {
 	return newFunctions(c, namespace)
 }
 
+func (c *PorchV1alpha1Client) Packages(namespace string) PackageInterface {
+	return newPackages(c, namespace)
+}
+
 func (c *PorchV1alpha1Client) PackageRevisions(namespace string) PackageRevisionInterface {
 	return newPackageRevisions(c, namespace)
 }
diff --git a/porch/api/generated/informers/externalversions/generic.go b/porch/api/generated/informers/externalversions/generic.go
index 2d571463..d3db7dd7 100644
--- a/porch/api/generated/informers/externalversions/generic.go
+++ b/porch/api/generated/informers/externalversions/generic.go
@@ -53,6 +53,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource
 	// Group=porch.kpt.dev, Version=v1alpha1
 	case v1alpha1.SchemeGroupVersion.WithResource("functions"):
 		return &genericInformer{resource: resource.GroupResource(), informer: f.Porch().V1alpha1().Functions().Informer()}, nil
+	case v1alpha1.SchemeGroupVersion.WithResource("packages"):
+		return &genericInformer{resource: resource.GroupResource(), informer: f.Porch().V1alpha1().Packages().Informer()}, nil
 	case v1alpha1.SchemeGroupVersion.WithResource("packagerevisions"):
 		return &genericInformer{resource: resource.GroupResource(), informer: f.Porch().V1alpha1().PackageRevisions().Informer()}, nil
 	case v1alpha1.SchemeGroupVersion.WithResource("packagerevisionresources"):
diff --git a/porch/api/generated/informers/externalversions/porch/v1alpha1/interface.go b/porch/api/generated/informers/externalversions/porch/v1alpha1/interface.go
index 16b7c4aa..d6d1b18e 100644
--- a/porch/api/generated/informers/externalversions/porch/v1alpha1/interface.go
+++ b/porch/api/generated/informers/externalversions/porch/v1alpha1/interface.go
@@ -24,6 +24,8 @@ import (
 type Interface interface {
 	// Functions returns a FunctionInformer.
 	Functions() FunctionInformer
+	// Packages returns a PackageInformer.
+	Packages() PackageInformer
 	// PackageRevisions returns a PackageRevisionInformer.
 	PackageRevisions() PackageRevisionInformer
 	// PackageRevisionResources returns a PackageRevisionResourcesInformer.
@@ -46,6 +48,11 @@ func (v *version) Functions() FunctionInformer {
 	return &functionInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
 }
 
+// Packages returns a PackageInformer.
+func (v *version) Packages() PackageInformer {
+	return &packageInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
+}
+
 // PackageRevisions returns a PackageRevisionInformer.
 func (v *version) PackageRevisions() PackageRevisionInformer {
 	return &packageRevisionInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
diff --git a/porch/api/generated/informers/externalversions/porch/v1alpha1/package.go b/porch/api/generated/informers/externalversions/porch/v1alpha1/package.go
new file mode 100644
index 00000000..2ca2ce4f
--- /dev/null
+++ b/porch/api/generated/informers/externalversions/porch/v1alpha1/package.go
@@ -0,0 +1,88 @@
+// 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.
+
+// Code generated by informer-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	"context"
+	time "time"
+
+	versioned "github.com/GoogleContainerTools/kpt/porch/api/generated/clientset/versioned"
+	internalinterfaces "github.com/GoogleContainerTools/kpt/porch/api/generated/informers/externalversions/internalinterfaces"
+	v1alpha1 "github.com/GoogleContainerTools/kpt/porch/api/generated/listers/porch/v1alpha1"
+	porchv1alpha1 "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+	watch "k8s.io/apimachinery/pkg/watch"
+	cache "k8s.io/client-go/tools/cache"
+)
+
+// PackageInformer provides access to a shared informer and lister for
+// Packages.
+type PackageInformer interface {
+	Informer() cache.SharedIndexInformer
+	Lister() v1alpha1.PackageLister
+}
+
+type packageInformer struct {
+	factory          internalinterfaces.SharedInformerFactory
+	tweakListOptions internalinterfaces.TweakListOptionsFunc
+	namespace        string
+}
+
+// NewPackageInformer constructs a new informer for Package type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewPackageInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
+	return NewFilteredPackageInformer(client, namespace, resyncPeriod, indexers, nil)
+}
+
+// NewFilteredPackageInformer constructs a new informer for Package type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewFilteredPackageInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
+	return cache.NewSharedIndexInformer(
+		&cache.ListWatch{
+			ListFunc: func(options v1.ListOptions) (runtime.Object, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.PorchV1alpha1().Packages(namespace).List(context.TODO(), options)
+			},
+			WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
+				if tweakListOptions != nil {
+					tweakListOptions(&options)
+				}
+				return client.PorchV1alpha1().Packages(namespace).Watch(context.TODO(), options)
+			},
+		},
+		&porchv1alpha1.Package{},
+		resyncPeriod,
+		indexers,
+	)
+}
+
+func (f *packageInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
+	return NewFilteredPackageInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
+}
+
+func (f *packageInformer) Informer() cache.SharedIndexInformer {
+	return f.factory.InformerFor(&porchv1alpha1.Package{}, f.defaultInformer)
+}
+
+func (f *packageInformer) Lister() v1alpha1.PackageLister {
+	return v1alpha1.NewPackageLister(f.Informer().GetIndexer())
+}
diff --git a/porch/api/generated/listers/porch/v1alpha1/expansion_generated.go b/porch/api/generated/listers/porch/v1alpha1/expansion_generated.go
index e20b2280..70a2f25c 100644
--- a/porch/api/generated/listers/porch/v1alpha1/expansion_generated.go
+++ b/porch/api/generated/listers/porch/v1alpha1/expansion_generated.go
@@ -24,6 +24,14 @@ type FunctionListerExpansion interface{}
 // FunctionNamespaceLister.
 type FunctionNamespaceListerExpansion interface{}
 
+// PackageListerExpansion allows custom methods to be added to
+// PackageLister.
+type PackageListerExpansion interface{}
+
+// PackageNamespaceListerExpansion allows custom methods to be added to
+// PackageNamespaceLister.
+type PackageNamespaceListerExpansion interface{}
+
 // PackageRevisionListerExpansion allows custom methods to be added to
 // PackageRevisionLister.
 type PackageRevisionListerExpansion interface{}
diff --git a/porch/api/generated/listers/porch/v1alpha1/package.go b/porch/api/generated/listers/porch/v1alpha1/package.go
new file mode 100644
index 00000000..f61cf6b5
--- /dev/null
+++ b/porch/api/generated/listers/porch/v1alpha1/package.go
@@ -0,0 +1,97 @@
+// 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.
+
+// Code generated by lister-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	v1alpha1 "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1"
+	"k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/client-go/tools/cache"
+)
+
+// PackageLister helps list Packages.
+// All objects returned here must be treated as read-only.
+type PackageLister interface {
+	// List lists all Packages in the indexer.
+	// Objects returned here must be treated as read-only.
+	List(selector labels.Selector) (ret []*v1alpha1.Package, err error)
+	// Packages returns an object that can list and get Packages.
+	Packages(namespace string) PackageNamespaceLister
+	PackageListerExpansion
+}
+
+// packageLister implements the PackageLister interface.
+type packageLister struct {
+	indexer cache.Indexer
+}
+
+// NewPackageLister returns a new PackageLister.
+func NewPackageLister(indexer cache.Indexer) PackageLister {
+	return &packageLister{indexer: indexer}
+}
+
+// List lists all Packages in the indexer.
+func (s *packageLister) List(selector labels.Selector) (ret []*v1alpha1.Package, err error) {
+	err = cache.ListAll(s.indexer, selector, func(m interface{}) {
+		ret = append(ret, m.(*v1alpha1.Package))
+	})
+	return ret, err
+}
+
+// Packages returns an object that can list and get Packages.
+func (s *packageLister) Packages(namespace string) PackageNamespaceLister {
+	return packageNamespaceLister{indexer: s.indexer, namespace: namespace}
+}
+
+// PackageNamespaceLister helps list and get Packages.
+// All objects returned here must be treated as read-only.
+type PackageNamespaceLister interface {
+	// List lists all Packages in the indexer for a given namespace.
+	// Objects returned here must be treated as read-only.
+	List(selector labels.Selector) (ret []*v1alpha1.Package, err error)
+	// Get retrieves the Package from the indexer for a given namespace and name.
+	// Objects returned here must be treated as read-only.
+	Get(name string) (*v1alpha1.Package, error)
+	PackageNamespaceListerExpansion
+}
+
+// packageNamespaceLister implements the PackageNamespaceLister
+// interface.
+type packageNamespaceLister struct {
+	indexer   cache.Indexer
+	namespace string
+}
+
+// List lists all Packages in the indexer for a given namespace.
+func (s packageNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.Package, err error) {
+	err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) {
+		ret = append(ret, m.(*v1alpha1.Package))
+	})
+	return ret, err
+}
+
+// Get retrieves the Package from the indexer for a given namespace and name.
+func (s packageNamespaceLister) Get(name string) (*v1alpha1.Package, error) {
+	obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name)
+	if err != nil {
+		return nil, err
+	}
+	if !exists {
+		return nil, errors.NewNotFound(v1alpha1.Resource("package"), name)
+	}
+	return obj.(*v1alpha1.Package), nil
+}
diff --git a/porch/api/generated/openapi/zz_generated.openapi.go b/porch/api/generated/openapi/zz_generated.openapi.go
index db93649a..25e81431 100644
--- a/porch/api/generated/openapi/zz_generated.openapi.go
+++ b/porch/api/generated/openapi/zz_generated.openapi.go
@@ -39,9 +39,11 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
 		"github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.GitLock":                      schema_porch_api_porch_v1alpha1_GitLock(ref),
 		"github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.GitPackage":                   schema_porch_api_porch_v1alpha1_GitPackage(ref),
 		"github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.OciPackage":                   schema_porch_api_porch_v1alpha1_OciPackage(ref),
+		"github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.Package":                      schema_porch_api_porch_v1alpha1_Package(ref),
 		"github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.PackageCloneTaskSpec":         schema_porch_api_porch_v1alpha1_PackageCloneTaskSpec(ref),
 		"github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.PackageEditTaskSpec":          schema_porch_api_porch_v1alpha1_PackageEditTaskSpec(ref),
 		"github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.PackageInitTaskSpec":          schema_porch_api_porch_v1alpha1_PackageInitTaskSpec(ref),
+		"github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.PackageList":                  schema_porch_api_porch_v1alpha1_PackageList(ref),
 		"github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.PackagePatchTaskSpec":         schema_porch_api_porch_v1alpha1_PackagePatchTaskSpec(ref),
 		"github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.PackageRevision":              schema_porch_api_porch_v1alpha1_PackageRevision(ref),
 		"github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.PackageRevisionList":          schema_porch_api_porch_v1alpha1_PackageRevisionList(ref),
@@ -51,6 +53,8 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
 		"github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.PackageRevisionResourcesSpec": schema_porch_api_porch_v1alpha1_PackageRevisionResourcesSpec(ref),
 		"github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.PackageRevisionSpec":          schema_porch_api_porch_v1alpha1_PackageRevisionSpec(ref),
 		"github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.PackageRevisionStatus":        schema_porch_api_porch_v1alpha1_PackageRevisionStatus(ref),
+		"github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.PackageSpec":                  schema_porch_api_porch_v1alpha1_PackageSpec(ref),
+		"github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.PackageStatus":                schema_porch_api_porch_v1alpha1_PackageStatus(ref),
 		"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),
@@ -562,6 +566,53 @@ func schema_porch_api_porch_v1alpha1_OciPackage(ref common.ReferenceCallback) co
 	}
 }
 
+func schema_porch_api_porch_v1alpha1_Package(ref common.ReferenceCallback) common.OpenAPIDefinition {
+	return common.OpenAPIDefinition{
+		Schema: spec.Schema{
+			SchemaProps: spec.SchemaProps{
+				Description: "Package",
+				Type:        []string{"object"},
+				Properties: map[string]spec.Schema{
+					"kind": {
+						SchemaProps: spec.SchemaProps{
+							Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
+							Type:        []string{"string"},
+							Format:      "",
+						},
+					},
+					"apiVersion": {
+						SchemaProps: spec.SchemaProps{
+							Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
+							Type:        []string{"string"},
+							Format:      "",
+						},
+					},
+					"metadata": {
+						SchemaProps: spec.SchemaProps{
+							Default: map[string]interface{}{},
+							Ref:     ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"),
+						},
+					},
+					"spec": {
+						SchemaProps: spec.SchemaProps{
+							Default: map[string]interface{}{},
+							Ref:     ref("github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.PackageSpec"),
+						},
+					},
+					"status": {
+						SchemaProps: spec.SchemaProps{
+							Default: map[string]interface{}{},
+							Ref:     ref("github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.PackageStatus"),
+						},
+					},
+				},
+			},
+		},
+		Dependencies: []string{
+			"github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.PackageSpec", "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.PackageStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
+	}
+}
+
 func schema_porch_api_porch_v1alpha1_PackageCloneTaskSpec(ref common.ReferenceCallback) common.OpenAPIDefinition {
 	return common.OpenAPIDefinition{
 		Schema: spec.Schema{
@@ -658,6 +709,55 @@ func schema_porch_api_porch_v1alpha1_PackageInitTaskSpec(ref common.ReferenceCal
 	}
 }
 
+func schema_porch_api_porch_v1alpha1_PackageList(ref common.ReferenceCallback) common.OpenAPIDefinition {
+	return common.OpenAPIDefinition{
+		Schema: spec.Schema{
+			SchemaProps: spec.SchemaProps{
+				Description: "PackageList",
+				Type:        []string{"object"},
+				Properties: map[string]spec.Schema{
+					"kind": {
+						SchemaProps: spec.SchemaProps{
+							Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
+							Type:        []string{"string"},
+							Format:      "",
+						},
+					},
+					"apiVersion": {
+						SchemaProps: spec.SchemaProps{
+							Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
+							Type:        []string{"string"},
+							Format:      "",
+						},
+					},
+					"metadata": {
+						SchemaProps: spec.SchemaProps{
+							Default: map[string]interface{}{},
+							Ref:     ref("k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"),
+						},
+					},
+					"items": {
+						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.Package"),
+									},
+								},
+							},
+						},
+					},
+				},
+				Required: []string{"items"},
+			},
+		},
+		Dependencies: []string{
+			"github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1.Package", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
+	}
+}
+
 func schema_porch_api_porch_v1alpha1_PackagePatchTaskSpec(ref common.ReferenceCallback) common.OpenAPIDefinition {
 	return common.OpenAPIDefinition{
 		Schema: spec.Schema{
@@ -1037,6 +1137,53 @@ func schema_porch_api_porch_v1alpha1_PackageRevisionStatus(ref common.ReferenceC
 	}
 }
 
+func schema_porch_api_porch_v1alpha1_PackageSpec(ref common.ReferenceCallback) common.OpenAPIDefinition {
+	return common.OpenAPIDefinition{
+		Schema: spec.Schema{
+			SchemaProps: spec.SchemaProps{
+				Description: "PackageSpec defines the desired state of Package",
+				Type:        []string{"object"},
+				Properties: map[string]spec.Schema{
+					"packageName": {
+						SchemaProps: spec.SchemaProps{
+							Description: "PackageName identifies the package in the repository.",
+							Type:        []string{"string"},
+							Format:      "",
+						},
+					},
+					"repository": {
+						SchemaProps: spec.SchemaProps{
+							Description: "RepositoryName is the name of the Repository object containing this package.",
+							Type:        []string{"string"},
+							Format:      "",
+						},
+					},
+				},
+			},
+		},
+	}
+}
+
+func schema_porch_api_porch_v1alpha1_PackageStatus(ref common.ReferenceCallback) common.OpenAPIDefinition {
+	return common.OpenAPIDefinition{
+		Schema: spec.Schema{
+			SchemaProps: spec.SchemaProps{
+				Description: "PackageStatus defines the observed state of Package",
+				Type:        []string{"object"},
+				Properties: map[string]spec.Schema{
+					"latestRevision": {
+						SchemaProps: spec.SchemaProps{
+							Description: "LatestRevision identifies the package revision that is the latest published package revision belonging to this package",
+							Type:        []string{"string"},
+							Format:      "",
+						},
+					},
+				},
+			},
+		},
+	}
+}
+
 func schema_porch_api_porch_v1alpha1_PackageUpdateTaskSpec(ref common.ReferenceCallback) common.OpenAPIDefinition {
 	return common.OpenAPIDefinition{
 		Schema: spec.Schema{
diff --git a/porch/api/go.mod b/porch/api/go.mod
index abaf1801..9b4e3a82 100644
--- a/porch/api/go.mod
+++ b/porch/api/go.mod
@@ -59,3 +59,5 @@ require (
 	sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect
 	sigs.k8s.io/yaml v1.3.0 // indirect
 )
+
+replace k8s.io/code-generator => github.com/platkrm/code-generator v0.23.5-porch.1
diff --git a/porch/api/go.sum b/porch/api/go.sum
index 203c38eb..58e246c8 100644
--- a/porch/api/go.sum
+++ b/porch/api/go.sum
@@ -238,6 +238,8 @@ github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAl
 github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/platkrm/code-generator v0.23.5-porch.1 h1:SMWdEx8urgCWrQzm2SbJyp/64oNw4wHheW3rVH2CbnQ=
+github.com/platkrm/code-generator v0.23.5-porch.1/go.mod h1:S0Q1JVA+kSzTI1oUvbKAxZY/DYbA/ZUb4Uknog12ETk=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@@ -644,8 +646,6 @@ k8s.io/apimachinery v0.23.5 h1:Va7dwhp8wgkUPWsEXk6XglXWU4IKYLKNlv8VkX7SDM0=
 k8s.io/apimachinery v0.23.5/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM=
 k8s.io/client-go v0.23.5 h1:zUXHmEuqx0RY4+CsnkOn5l0GU+skkRXKGJrhmE2SLd8=
 k8s.io/client-go v0.23.5/go.mod h1:flkeinTO1CirYgzMPRWxUCnV0G4Fbu2vLhYCObnt/r4=
-k8s.io/code-generator v0.23.5 h1:xn3a6J5pUL49AoH6SPrOFtnB5cvdMl76f/bEY176R3c=
-k8s.io/code-generator v0.23.5/go.mod h1:S0Q1JVA+kSzTI1oUvbKAxZY/DYbA/ZUb4Uknog12ETk=
 k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c h1:GohjlNKauSai7gN4wsJkeZ3WAJx4Sh+oT/b5IYn5suA=
 k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
 k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
diff --git a/porch/api/porch/register.go b/porch/api/porch/register.go
index be829722..06492a7c 100644
--- a/porch/api/porch/register.go
+++ b/porch/api/porch/register.go
@@ -38,6 +38,8 @@ var (
 
 func addKnownTypes(scheme *runtime.Scheme) error {
 	scheme.AddKnownTypes(SchemeGroupVersion,
+		&Package{},
+		&PackageList{},
 		&PackageRevision{},
 		&PackageRevisionList{},
 		&PackageRevisionResources{},
diff --git a/porch/api/porch/types_package.go b/porch/api/porch/types_package.go
new file mode 100644
index 00000000..2cbced29
--- /dev/null
+++ b/porch/api/porch/types_package.go
@@ -0,0 +1,57 @@
+// 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 porch
+
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// +genclient
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+
+// Package
+// +k8s:openapi-gen=true
+type Package struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	Spec   PackageSpec   `json:"spec,omitempty"`
+	Status PackageStatus `json:"status,omitempty"`
+}
+
+// PackageList
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+type PackageList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty"`
+
+	Items []Package `json:"items"`
+}
+
+// PackageSpec defines the desired state of Package
+type PackageSpec struct {
+	// PackageName identifies the package in the repository.
+	PackageName string `json:"packageName,omitempty"`
+
+	// RepositoryName is the name of the Repository object containing this package.
+	RepositoryName string `json:"repository,omitempty"`
+}
+
+// PackageStatus defines the observed state of Package
+type PackageStatus struct {
+	// LatestRevision identifies the package revision that is the latest
+	// published package revision belonging to this package
+	LatestRevision string `json:"latestRevision,omitempty"`
+}
diff --git a/porch/api/porch/v1alpha1/register.go b/porch/api/porch/v1alpha1/register.go
index 63d0d46c..45714eb3 100644
--- a/porch/api/porch/v1alpha1/register.go
+++ b/porch/api/porch/v1alpha1/register.go
@@ -28,6 +28,7 @@ var (
 	localSchemeBuilder = &SchemeBuilder
 	AddToScheme        = localSchemeBuilder.AddToScheme
 
+	PackageGVR                  = SchemeGroupVersion.WithResource("packages")
 	PackageRevisionGVR          = SchemeGroupVersion.WithResource("packagerevisions")
 	PackageRevisionResourcesGVR = SchemeGroupVersion.WithResource("packagerevisionresources")
 	FunctionGVR                 = SchemeGroupVersion.WithResource("functions")
@@ -42,6 +43,8 @@ func addKnownTypes(scheme *runtime.Scheme) error {
 	// +kubebuilder:scaffold:install
 
 	scheme.AddKnownTypes(SchemeGroupVersion,
+		&Package{},
+		&PackageList{},
 		&PackageRevision{},
 		&PackageRevisionList{},
 		&PackageRevisionResources{},
diff --git a/porch/api/porch/v1alpha1/types_package.go b/porch/api/porch/v1alpha1/types_package.go
new file mode 100644
index 00000000..ec824f3f
--- /dev/null
+++ b/porch/api/porch/v1alpha1/types_package.go
@@ -0,0 +1,57 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package v1alpha1
+
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// +genclient
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+
+// Package
+// +k8s:openapi-gen=true
+type Package struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	Spec   PackageSpec   `json:"spec,omitempty"`
+	Status PackageStatus `json:"status,omitempty"`
+}
+
+// PackageList
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+type PackageList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty"`
+
+	Items []Package `json:"items"`
+}
+
+// PackageSpec defines the desired state of Package
+type PackageSpec struct {
+	// PackageName identifies the package in the repository.
+	PackageName string `json:"packageName,omitempty"`
+
+	// RepositoryName is the name of the Repository object containing this package.
+	RepositoryName string `json:"repository,omitempty"`
+}
+
+// PackageStatus defines the observed state of Package
+type PackageStatus struct {
+	// LatestRevision identifies the package revision that is the latest
+	// published package revision belonging to this package
+	LatestRevision string `json:"latestRevision,omitempty"`
+}
diff --git a/porch/api/porch/v1alpha1/zz_generated.conversion.go b/porch/api/porch/v1alpha1/zz_generated.conversion.go
index 7b85ab8b..2df2d4d9 100644
--- a/porch/api/porch/v1alpha1/zz_generated.conversion.go
+++ b/porch/api/porch/v1alpha1/zz_generated.conversion.go
@@ -134,6 +134,16 @@ func RegisterConversions(s *runtime.Scheme) error {
 	}); err != nil {
 		return err
 	}
+	if err := s.AddGeneratedConversionFunc((*Package)(nil), (*porch.Package)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_v1alpha1_Package_To_porch_Package(a.(*Package), b.(*porch.Package), scope)
+	}); err != nil {
+		return err
+	}
+	if err := s.AddGeneratedConversionFunc((*porch.Package)(nil), (*Package)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_porch_Package_To_v1alpha1_Package(a.(*porch.Package), b.(*Package), scope)
+	}); err != nil {
+		return err
+	}
 	if err := s.AddGeneratedConversionFunc((*PackageCloneTaskSpec)(nil), (*porch.PackageCloneTaskSpec)(nil), func(a, b interface{}, scope conversion.Scope) error {
 		return Convert_v1alpha1_PackageCloneTaskSpec_To_porch_PackageCloneTaskSpec(a.(*PackageCloneTaskSpec), b.(*porch.PackageCloneTaskSpec), scope)
 	}); err != nil {
@@ -164,6 +174,16 @@ func RegisterConversions(s *runtime.Scheme) error {
 	}); err != nil {
 		return err
 	}
+	if err := s.AddGeneratedConversionFunc((*PackageList)(nil), (*porch.PackageList)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_v1alpha1_PackageList_To_porch_PackageList(a.(*PackageList), b.(*porch.PackageList), scope)
+	}); err != nil {
+		return err
+	}
+	if err := s.AddGeneratedConversionFunc((*porch.PackageList)(nil), (*PackageList)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_porch_PackageList_To_v1alpha1_PackageList(a.(*porch.PackageList), b.(*PackageList), scope)
+	}); err != nil {
+		return err
+	}
 	if err := s.AddGeneratedConversionFunc((*PackagePatchTaskSpec)(nil), (*porch.PackagePatchTaskSpec)(nil), func(a, b interface{}, scope conversion.Scope) error {
 		return Convert_v1alpha1_PackagePatchTaskSpec_To_porch_PackagePatchTaskSpec(a.(*PackagePatchTaskSpec), b.(*porch.PackagePatchTaskSpec), scope)
 	}); err != nil {
@@ -254,6 +274,26 @@ func RegisterConversions(s *runtime.Scheme) error {
 	}); err != nil {
 		return err
 	}
+	if err := s.AddGeneratedConversionFunc((*PackageSpec)(nil), (*porch.PackageSpec)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_v1alpha1_PackageSpec_To_porch_PackageSpec(a.(*PackageSpec), b.(*porch.PackageSpec), scope)
+	}); err != nil {
+		return err
+	}
+	if err := s.AddGeneratedConversionFunc((*porch.PackageSpec)(nil), (*PackageSpec)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_porch_PackageSpec_To_v1alpha1_PackageSpec(a.(*porch.PackageSpec), b.(*PackageSpec), scope)
+	}); err != nil {
+		return err
+	}
+	if err := s.AddGeneratedConversionFunc((*PackageStatus)(nil), (*porch.PackageStatus)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_v1alpha1_PackageStatus_To_porch_PackageStatus(a.(*PackageStatus), b.(*porch.PackageStatus), scope)
+	}); err != nil {
+		return err
+	}
+	if err := s.AddGeneratedConversionFunc((*porch.PackageStatus)(nil), (*PackageStatus)(nil), func(a, b interface{}, scope conversion.Scope) error {
+		return Convert_porch_PackageStatus_To_v1alpha1_PackageStatus(a.(*porch.PackageStatus), b.(*PackageStatus), scope)
+	}); err != nil {
+		return err
+	}
 	if err := s.AddGeneratedConversionFunc((*PackageUpdateTaskSpec)(nil), (*porch.PackageUpdateTaskSpec)(nil), func(a, b interface{}, scope conversion.Scope) error {
 		return Convert_v1alpha1_PackageUpdateTaskSpec_To_porch_PackageUpdateTaskSpec(a.(*PackageUpdateTaskSpec), b.(*porch.PackageUpdateTaskSpec), scope)
 	}); err != nil {
@@ -609,6 +649,38 @@ func Convert_porch_OciPackage_To_v1alpha1_OciPackage(in *porch.OciPackage, out *
 	return autoConvert_porch_OciPackage_To_v1alpha1_OciPackage(in, out, s)
 }
 
+func autoConvert_v1alpha1_Package_To_porch_Package(in *Package, out *porch.Package, s conversion.Scope) error {
+	out.ObjectMeta = in.ObjectMeta
+	if err := Convert_v1alpha1_PackageSpec_To_porch_PackageSpec(&in.Spec, &out.Spec, s); err != nil {
+		return err
+	}
+	if err := Convert_v1alpha1_PackageStatus_To_porch_PackageStatus(&in.Status, &out.Status, s); err != nil {
+		return err
+	}
+	return nil
+}
+
+// Convert_v1alpha1_Package_To_porch_Package is an autogenerated conversion function.
+func Convert_v1alpha1_Package_To_porch_Package(in *Package, out *porch.Package, s conversion.Scope) error {
+	return autoConvert_v1alpha1_Package_To_porch_Package(in, out, s)
+}
+
+func autoConvert_porch_Package_To_v1alpha1_Package(in *porch.Package, out *Package, s conversion.Scope) error {
+	out.ObjectMeta = in.ObjectMeta
+	if err := Convert_porch_PackageSpec_To_v1alpha1_PackageSpec(&in.Spec, &out.Spec, s); err != nil {
+		return err
+	}
+	if err := Convert_porch_PackageStatus_To_v1alpha1_PackageStatus(&in.Status, &out.Status, s); err != nil {
+		return err
+	}
+	return nil
+}
+
+// Convert_porch_Package_To_v1alpha1_Package is an autogenerated conversion function.
+func Convert_porch_Package_To_v1alpha1_Package(in *porch.Package, out *Package, s conversion.Scope) error {
+	return autoConvert_porch_Package_To_v1alpha1_Package(in, out, s)
+}
+
 func autoConvert_v1alpha1_PackageCloneTaskSpec_To_porch_PackageCloneTaskSpec(in *PackageCloneTaskSpec, out *porch.PackageCloneTaskSpec, s conversion.Scope) error {
 	if err := Convert_v1alpha1_UpstreamPackage_To_porch_UpstreamPackage(&in.Upstream, &out.Upstream, s); err != nil {
 		return err
@@ -681,6 +753,28 @@ func Convert_porch_PackageInitTaskSpec_To_v1alpha1_PackageInitTaskSpec(in *porch
 	return autoConvert_porch_PackageInitTaskSpec_To_v1alpha1_PackageInitTaskSpec(in, out, s)
 }
 
+func autoConvert_v1alpha1_PackageList_To_porch_PackageList(in *PackageList, out *porch.PackageList, s conversion.Scope) error {
+	out.ListMeta = in.ListMeta
+	out.Items = *(*[]porch.Package)(unsafe.Pointer(&in.Items))
+	return nil
+}
+
+// Convert_v1alpha1_PackageList_To_porch_PackageList is an autogenerated conversion function.
+func Convert_v1alpha1_PackageList_To_porch_PackageList(in *PackageList, out *porch.PackageList, s conversion.Scope) error {
+	return autoConvert_v1alpha1_PackageList_To_porch_PackageList(in, out, s)
+}
+
+func autoConvert_porch_PackageList_To_v1alpha1_PackageList(in *porch.PackageList, out *PackageList, s conversion.Scope) error {
+	out.ListMeta = in.ListMeta
+	out.Items = *(*[]Package)(unsafe.Pointer(&in.Items))
+	return nil
+}
+
+// Convert_porch_PackageList_To_v1alpha1_PackageList is an autogenerated conversion function.
+func Convert_porch_PackageList_To_v1alpha1_PackageList(in *porch.PackageList, out *PackageList, s conversion.Scope) error {
+	return autoConvert_porch_PackageList_To_v1alpha1_PackageList(in, out, s)
+}
+
 func autoConvert_v1alpha1_PackagePatchTaskSpec_To_porch_PackagePatchTaskSpec(in *PackagePatchTaskSpec, out *porch.PackagePatchTaskSpec, s conversion.Scope) error {
 	out.Patches = *(*[]porch.PatchSpec)(unsafe.Pointer(&in.Patches))
 	return nil
@@ -903,6 +997,48 @@ func Convert_porch_PackageRevisionStatus_To_v1alpha1_PackageRevisionStatus(in *p
 	return autoConvert_porch_PackageRevisionStatus_To_v1alpha1_PackageRevisionStatus(in, out, s)
 }
 
+func autoConvert_v1alpha1_PackageSpec_To_porch_PackageSpec(in *PackageSpec, out *porch.PackageSpec, s conversion.Scope) error {
+	out.PackageName = in.PackageName
+	out.RepositoryName = in.RepositoryName
+	return nil
+}
+
+// Convert_v1alpha1_PackageSpec_To_porch_PackageSpec is an autogenerated conversion function.
+func Convert_v1alpha1_PackageSpec_To_porch_PackageSpec(in *PackageSpec, out *porch.PackageSpec, s conversion.Scope) error {
+	return autoConvert_v1alpha1_PackageSpec_To_porch_PackageSpec(in, out, s)
+}
+
+func autoConvert_porch_PackageSpec_To_v1alpha1_PackageSpec(in *porch.PackageSpec, out *PackageSpec, s conversion.Scope) error {
+	out.PackageName = in.PackageName
+	out.RepositoryName = in.RepositoryName
+	return nil
+}
+
+// Convert_porch_PackageSpec_To_v1alpha1_PackageSpec is an autogenerated conversion function.
+func Convert_porch_PackageSpec_To_v1alpha1_PackageSpec(in *porch.PackageSpec, out *PackageSpec, s conversion.Scope) error {
+	return autoConvert_porch_PackageSpec_To_v1alpha1_PackageSpec(in, out, s)
+}
+
+func autoConvert_v1alpha1_PackageStatus_To_porch_PackageStatus(in *PackageStatus, out *porch.PackageStatus, s conversion.Scope) error {
+	out.LatestRevision = in.LatestRevision
+	return nil
+}
+
+// Convert_v1alpha1_PackageStatus_To_porch_PackageStatus is an autogenerated conversion function.
+func Convert_v1alpha1_PackageStatus_To_porch_PackageStatus(in *PackageStatus, out *porch.PackageStatus, s conversion.Scope) error {
+	return autoConvert_v1alpha1_PackageStatus_To_porch_PackageStatus(in, out, s)
+}
+
+func autoConvert_porch_PackageStatus_To_v1alpha1_PackageStatus(in *porch.PackageStatus, out *PackageStatus, s conversion.Scope) error {
+	out.LatestRevision = in.LatestRevision
+	return nil
+}
+
+// Convert_porch_PackageStatus_To_v1alpha1_PackageStatus is an autogenerated conversion function.
+func Convert_porch_PackageStatus_To_v1alpha1_PackageStatus(in *porch.PackageStatus, out *PackageStatus, s conversion.Scope) error {
+	return autoConvert_porch_PackageStatus_To_v1alpha1_PackageStatus(in, out, s)
+}
+
 func autoConvert_v1alpha1_PackageUpdateTaskSpec_To_porch_PackageUpdateTaskSpec(in *PackageUpdateTaskSpec, out *porch.PackageUpdateTaskSpec, s conversion.Scope) error {
 	if err := Convert_v1alpha1_UpstreamPackage_To_porch_UpstreamPackage(&in.Upstream, &out.Upstream, s); err != nil {
 		return err
diff --git a/porch/api/porch/v1alpha1/zz_generated.deepcopy.go b/porch/api/porch/v1alpha1/zz_generated.deepcopy.go
index f4742812..24cbb1ab 100644
--- a/porch/api/porch/v1alpha1/zz_generated.deepcopy.go
+++ b/porch/api/porch/v1alpha1/zz_generated.deepcopy.go
@@ -251,6 +251,34 @@ func (in *OciPackage) DeepCopy() *OciPackage {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Package) DeepCopyInto(out *Package) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	out.Spec = in.Spec
+	out.Status = in.Status
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Package.
+func (in *Package) DeepCopy() *Package {
+	if in == nil {
+		return nil
+	}
+	out := new(Package)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *Package) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *PackageCloneTaskSpec) DeepCopyInto(out *PackageCloneTaskSpec) {
 	*out = *in
@@ -310,6 +338,39 @@ func (in *PackageInitTaskSpec) DeepCopy() *PackageInitTaskSpec {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PackageList) DeepCopyInto(out *PackageList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]Package, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageList.
+func (in *PackageList) DeepCopy() *PackageList {
+	if in == nil {
+		return nil
+	}
+	out := new(PackageList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *PackageList) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *PackagePatchTaskSpec) DeepCopyInto(out *PackagePatchTaskSpec) {
 	*out = *in
@@ -541,6 +602,38 @@ func (in *PackageRevisionStatus) DeepCopy() *PackageRevisionStatus {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PackageSpec) DeepCopyInto(out *PackageSpec) {
+	*out = *in
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageSpec.
+func (in *PackageSpec) DeepCopy() *PackageSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(PackageSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PackageStatus) DeepCopyInto(out *PackageStatus) {
+	*out = *in
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageStatus.
+func (in *PackageStatus) DeepCopy() *PackageStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(PackageStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *PackageUpdateTaskSpec) DeepCopyInto(out *PackageUpdateTaskSpec) {
 	*out = *in
diff --git a/porch/api/porch/zz_generated.deepcopy.go b/porch/api/porch/zz_generated.deepcopy.go
index 9491de08..cd31543f 100644
--- a/porch/api/porch/zz_generated.deepcopy.go
+++ b/porch/api/porch/zz_generated.deepcopy.go
@@ -251,6 +251,34 @@ func (in *OciPackage) DeepCopy() *OciPackage {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Package) DeepCopyInto(out *Package) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	out.Spec = in.Spec
+	out.Status = in.Status
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Package.
+func (in *Package) DeepCopy() *Package {
+	if in == nil {
+		return nil
+	}
+	out := new(Package)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *Package) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *PackageCloneTaskSpec) DeepCopyInto(out *PackageCloneTaskSpec) {
 	*out = *in
@@ -310,6 +338,39 @@ func (in *PackageInitTaskSpec) DeepCopy() *PackageInitTaskSpec {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PackageList) DeepCopyInto(out *PackageList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]Package, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageList.
+func (in *PackageList) DeepCopy() *PackageList {
+	if in == nil {
+		return nil
+	}
+	out := new(PackageList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *PackageList) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *PackagePatchTaskSpec) DeepCopyInto(out *PackagePatchTaskSpec) {
 	*out = *in
@@ -541,6 +602,38 @@ func (in *PackageRevisionStatus) DeepCopy() *PackageRevisionStatus {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PackageSpec) DeepCopyInto(out *PackageSpec) {
+	*out = *in
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageSpec.
+func (in *PackageSpec) DeepCopy() *PackageSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(PackageSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PackageStatus) DeepCopyInto(out *PackageStatus) {
+	*out = *in
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageStatus.
+func (in *PackageStatus) DeepCopy() *PackageStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(PackageStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *PackageUpdateTaskSpec) DeepCopyInto(out *PackageUpdateTaskSpec) {
 	*out = *in
diff --git a/porch/func/internal/podevaluator.go b/porch/func/internal/podevaluator.go
index 0cfcb2ac..37ab9975 100644
--- a/porch/func/internal/podevaluator.go
+++ b/porch/func/internal/podevaluator.go
@@ -26,7 +26,7 @@ import (
 	"time"
 
 	"github.com/GoogleContainerTools/kpt/porch/func/evaluator"
-	"github.com/google/go-containerregistry/pkg/authn"
+	"github.com/google/go-containerregistry/pkg/gcrane"
 	"github.com/google/go-containerregistry/pkg/name"
 	"github.com/google/go-containerregistry/pkg/v1/remote"
 	"google.golang.org/grpc"
@@ -421,8 +421,8 @@ func (pm *podManager) getFuncEvalPodClient(ctx context.Context, image string, tt
 	}
 }
 
-// imageEntrypoint get the entrypoint of a container image by looking at its metadata.
-func (pm *podManager) imageDigestAndEntrypoint(image string) (*digestAndEntrypoint, error) {
+// imageDigestAndEntrypoint gets the entrypoint of a container image by looking at its metadata.
+func (pm *podManager) imageDigestAndEntrypoint(ctx context.Context, image string) (*digestAndEntrypoint, error) {
 	start := time.Now()
 	defer func() {
 		klog.Infof("getting image metadata for %v took %v", image, time.Now().Sub(start))
@@ -432,7 +432,7 @@ func (pm *podManager) imageDigestAndEntrypoint(image string) (*digestAndEntrypoi
 	if err != nil {
 		return nil, err
 	}
-	img, err := remote.Image(ref, remote.WithAuthFromKeychain(authn.DefaultKeychain))
+	img, err := remote.Image(ref, remote.WithAuthFromKeychain(gcrane.Keychain), remote.WithContext(ctx))
 	if err != nil {
 		return nil, err
 	}
@@ -466,7 +466,7 @@ func (pm *podManager) retrieveOrCreatePod(ctx context.Context, image string, ttl
 	var err error
 	val, found := pm.imageMetadataCache.Load(image)
 	if !found {
-		de, err = pm.imageDigestAndEntrypoint(image)
+		de, err = pm.imageDigestAndEntrypoint(ctx, image)
 		if err != nil {
 			return client.ObjectKey{}, fmt.Errorf("unable to get the entrypoint for %v: %w", image, err)
 		}
diff --git a/porch/pkg/cache/package.go b/porch/pkg/cache/package.go
new file mode 100644
index 00000000..6fa7a0eb
--- /dev/null
+++ b/porch/pkg/cache/package.go
@@ -0,0 +1,37 @@
+// 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 cache
+
+import "github.com/GoogleContainerTools/kpt/porch/pkg/repository"
+
+// We take advantage of the cache having a global view of all the packages
+// in a repository and compute the latest package revision in the cache
+// rather than add another level of caching in the repositories themselves.
+// This also reuses the revision comparison code and ensures same behavior
+// between Git and OCI.
+
+var _ repository.Package = &cachedPackage{}
+
+type cachedPackage struct {
+	repository.Package
+	latestPackageRevision string
+}
+
+func (c *cachedPackage) GetLatestRevision() string {
+	if c.latestPackageRevision != "" {
+		return c.latestPackageRevision
+	}
+	return c.Package.GetLatestRevision()
+}
diff --git a/porch/pkg/cache/packagerevision.go b/porch/pkg/cache/packagerevision.go
new file mode 100644
index 00000000..a89e14d6
--- /dev/null
+++ b/porch/pkg/cache/packagerevision.go
@@ -0,0 +1,44 @@
+// 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 cache
+
+import (
+	"github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1"
+	"github.com/GoogleContainerTools/kpt/porch/pkg/repository"
+)
+
+// We take advantage of the cache having a global view of all the packages
+// in a repository and compute the latest package revision in the cache
+// rather than add another level of caching in the repositories themselves.
+// This also reuses the revision comparison code and ensures same behavior
+// between Git and OCI.
+
+var _ repository.PackageRevision = &cachedPackageRevision{}
+
+type cachedPackageRevision struct {
+	repository.PackageRevision
+	isLatestRevision bool
+}
+
+func (c *cachedPackageRevision) GetPackageRevision() *v1alpha1.PackageRevision {
+	rev := c.PackageRevision.GetPackageRevision()
+	if c.isLatestRevision {
+		if rev.Labels == nil {
+			rev.Labels = map[string]string{}
+		}
+		rev.Labels[v1alpha1.LatestPackageRevisionKey] = v1alpha1.LatestPackageRevisionValue
+	}
+	return rev
+}
diff --git a/porch/pkg/cache/repository.go b/porch/pkg/cache/repository.go
index 32243a19..87f6fb69 100644
--- a/porch/pkg/cache/repository.go
+++ b/porch/pkg/cache/repository.go
@@ -16,8 +16,6 @@ package cache
 
 import (
 	"context"
-	"sort"
-	"strings"
 	"sync"
 	"time"
 
@@ -25,49 +23,37 @@ import (
 	"github.com/GoogleContainerTools/kpt/porch/pkg/repository"
 	"go.opentelemetry.io/otel"
 	"go.opentelemetry.io/otel/trace"
-	"golang.org/x/mod/semver"
 	"k8s.io/klog/v2"
 )
 
 var tracer = otel.Tracer("cache")
 
+// We take advantage of the cache having a global view of all the packages
+// in a repository and compute the latest package revision in the cache
+// rather than add another level of caching in the repositories themselves.
+// This also reuses the revision comparison code and ensures same behavior
+// between Git and OCI.
+
+var _ repository.Repository = &cachedRepository{}
+var _ repository.FunctionRepository = &cachedRepository{}
+
 type cachedRepository struct {
 	id     string
 	repo   repository.Repository
 	cancel context.CancelFunc
 
-	mutex          sync.Mutex
-	cachedPackages []*cachedPackageRevision
+	mutex                  sync.Mutex
+	cachedPackageRevisions []*cachedPackageRevision
+	cachedPackages         []*cachedPackage
+
 	// TODO: Currently we support repositories with homogenous content (only packages xor functions). Model this more optimally?
 	cachedFunctions []repository.Function
 	// Error encountered on repository refresh by the refresh goroutine.
 	// This is returned back by the cache to the background goroutine when it calls periodicall to resync repositories.
-	refreshError error
-}
-
-// We take advantage of the cache having a global view of all the packages
-// in a repository and compute the latest package revision in the cache
-// rather than add another level of caching in the repositories themselves.
-// This also reuses the revision comparison code and ensures same behavior
-// between Git and OCI.
-type cachedPackageRevision struct {
-	repository.PackageRevision
-	isLatestRevision bool
-}
-
-func (c *cachedPackageRevision) GetPackageRevision() *v1alpha1.PackageRevision {
-	rev := c.PackageRevision.GetPackageRevision()
-	if c.isLatestRevision {
-		if rev.Labels == nil {
-			rev.Labels = map[string]string{}
-		}
-		rev.Labels[v1alpha1.LatestPackageRevisionKey] = v1alpha1.LatestPackageRevisionValue
-	}
-	return rev
+	refreshRevisionsError error
+	refreshPkgsError      error
 }
 
-var _ repository.PackageRevision = &cachedPackageRevision{}
-
 func newRepository(id string, repo repository.Repository) *cachedRepository {
 	ctx, cancel := context.WithCancel(context.Background())
 	r := &cachedRepository{
@@ -81,11 +67,8 @@ func newRepository(id string, repo repository.Repository) *cachedRepository {
 	return r
 }
 
-var _ repository.Repository = &cachedRepository{}
-var _ repository.FunctionRepository = &cachedRepository{}
-
 func (r *cachedRepository) ListPackageRevisions(ctx context.Context, filter repository.ListPackageRevisionFilter) ([]repository.PackageRevision, error) {
-	packages, err := r.getPackages(ctx, filter, false)
+	packages, err := r.getPackageRevisions(ctx, filter, false)
 	if err != nil {
 		return nil, err
 	}
@@ -104,14 +87,21 @@ func (r *cachedRepository) ListFunctions(ctx context.Context) ([]repository.Func
 func (r *cachedRepository) getRefreshError() error {
 	r.mutex.Lock()
 	defer r.mutex.Unlock()
-	return r.refreshError
+
+	// TODO: This should also check r.refreshPkgsError when
+	//   the package resource is fully supported.
+
+	return r.refreshRevisionsError
 }
 
-func (r *cachedRepository) getPackages(ctx context.Context, filter repository.ListPackageRevisionFilter, forceRefresh bool) ([]repository.PackageRevision, error) {
-	r.mutex.Lock()
-	packages := r.cachedPackages
-	err := r.refreshError
-	r.mutex.Unlock()
+func (r *cachedRepository) getPackageRevisions(ctx context.Context, filter repository.ListPackageRevisionFilter, forceRefresh bool) ([]repository.PackageRevision, error) {
+	var packages []*cachedPackageRevision
+	var err error
+
+	r.lock(func() {
+		packages = r.cachedPackageRevisions
+		err = r.refreshRevisionsError
+	})
 
 	if forceRefresh {
 		packages = nil
@@ -125,10 +115,10 @@ func (r *cachedRepository) getPackages(ctx context.Context, filter repository.Li
 			packages = toCachedPackageRevisionSlice(p)
 		}
 
-		r.mutex.Lock()
-		r.cachedPackages = packages
-		r.refreshError = err
-		r.mutex.Unlock()
+		r.lock(func() {
+			r.cachedPackageRevisions = packages
+			r.refreshRevisionsError = err
+		})
 	}
 
 	if err != nil {
@@ -138,6 +128,40 @@ func (r *cachedRepository) getPackages(ctx context.Context, filter repository.Li
 	return toPackageRevisionSlice(packages, filter), nil
 }
 
+func (r *cachedRepository) getPackages(ctx context.Context, filter repository.ListPackageFilter, forceRefresh bool) ([]repository.Package, error) {
+	var packages []*cachedPackage
+	var err error
+
+	r.lock(func() {
+		packages = r.cachedPackages
+		err = r.refreshPkgsError
+	})
+
+	if forceRefresh {
+		packages = nil
+	}
+
+	if packages == nil {
+		// TODO: Avoid simultaneous fetches?
+		// TODO: Push-down partial refresh?
+		p, err := r.repo.ListPackages(ctx, repository.ListPackageFilter{})
+		if err == nil {
+			packages = toCachedPackageSlice(p)
+		}
+
+		r.lock(func() {
+			r.cachedPackages = packages
+			r.refreshPkgsError = err
+		})
+	}
+
+	if err != nil {
+		return nil, err
+	}
+
+	return toPackageSlice(packages, filter), nil
+}
+
 func (r *cachedRepository) getFunctions(ctx context.Context, force bool) ([]repository.Function, error) {
 	var functions []repository.Function
 
@@ -179,10 +203,10 @@ func (r *cachedRepository) CreatePackageRevision(ctx context.Context, obj *v1alp
 	}, nil
 }
 
-func (r *cachedRepository) UpdatePackage(ctx context.Context, old repository.PackageRevision) (repository.PackageDraft, error) {
+func (r *cachedRepository) UpdatePackageRevision(ctx context.Context, old repository.PackageRevision) (repository.PackageDraft, error) {
 	// Unwrap
 	unwrapped := old.(*cachedPackageRevision).PackageRevision
-	created, err := r.repo.UpdatePackage(ctx, unwrapped)
+	created, err := r.repo.UpdatePackageRevision(ctx, unwrapped)
 	if err != nil {
 		return nil, err
 	}
@@ -198,9 +222,11 @@ func (r *cachedRepository) update(closed repository.PackageRevision) *cachedPack
 	defer r.mutex.Unlock()
 
 	cached := &cachedPackageRevision{PackageRevision: closed}
-	r.cachedPackages = updateOrAppend(r.cachedPackages, cached)
+	r.cachedPackageRevisions = updateOrAppend(r.cachedPackageRevisions, cached)
 	// Recompute latest package revisions.
-	identifyLatestRevisions(r.cachedPackages)
+	identifyLatestRevisions(r.cachedPackageRevisions)
+
+	// TODO: Update the latest revisions for the r.cachedPackages
 	return cached
 }
 
@@ -222,11 +248,35 @@ func (r *cachedRepository) DeletePackageRevision(ctx context.Context, old reposi
 		return err
 	}
 
-	r.mutex.Lock()
 	// TODO: Do something more efficient than a full cache flush
-	r.cachedPackages = nil
-	r.mutex.Unlock()
+	r.flush()
+
+	return nil
+}
+
+func (r *cachedRepository) ListPackages(ctx context.Context, filter repository.ListPackageFilter) ([]repository.Package, error) {
+	packages, err := r.getPackages(ctx, filter, false)
+	if err != nil {
+		return nil, err
+	}
+
+	return packages, nil
+}
+
+func (r *cachedRepository) CreatePackage(ctx context.Context, obj *v1alpha1.Package) (repository.Package, error) {
+	klog.Infoln("cachedRepository::CreatePackage")
+	return r.repo.CreatePackage(ctx, obj)
+}
+
+func (r *cachedRepository) DeletePackage(ctx context.Context, old repository.Package) error {
+	// Unwrap
+	unwrapped := old.(*cachedPackage).Package
+	if err := r.repo.DeletePackage(ctx, unwrapped); err != nil {
+		return err
+	}
 
+	// TODO: Do something more efficient than a full cache flush
+	r.flush()
 	return nil
 }
 
@@ -256,99 +306,27 @@ func (r *cachedRepository) pollOnce(ctx context.Context) {
 	ctx, span := tracer.Start(ctx, "Repository::pollOnce", trace.WithAttributes())
 	defer span.End()
 
-	if _, err := r.getPackages(ctx, repository.ListPackageRevisionFilter{}, true); err != nil {
+	if _, err := r.getPackageRevisions(ctx, repository.ListPackageRevisionFilter{}, true); err != nil {
 		klog.Warningf("error polling repo packages %s: %v", r.id, err)
 	}
+	// TODO: Uncomment when package resources are fully supported
+	//if _, err := r.getPackages(ctx, repository.ListPackageRevisionFilter{}, true); err != nil {
+	//	klog.Warningf("error polling repo packages %s: %v", r.id, err)
+	//}
 	if _, err := r.getFunctions(ctx, true); err != nil {
 		klog.Warningf("error polling repo functions %s: %v", r.id, err)
 	}
 }
 
-func toCachedPackageRevisionSlice(revisions []repository.PackageRevision) []*cachedPackageRevision {
-	result := make([]*cachedPackageRevision, len(revisions))
-	for i := range revisions {
-		current := &cachedPackageRevision{
-			PackageRevision:  revisions[i],
-			isLatestRevision: false,
-		}
-		result[i] = current
-	}
-	identifyLatestRevisions(result)
-	return result
-}
-
-func identifyLatestRevisions(result []*cachedPackageRevision) {
-	// Compute the latest among the different revisions of the same package.
-	// The map is keyed by the package name; Values are the latest revision found so far.
-	latest := map[string]*cachedPackageRevision{}
-	for _, current := range result {
-		current.isLatestRevision = false // Clear all values
-
-		// Check if the current package revision is more recent than the one seen so far.
-		// Only consider Published packages
-		if current.Lifecycle() != v1alpha1.PackageRevisionLifecyclePublished {
-			continue
-		}
-
-		currentKey := current.Key()
-		if previous, ok := latest[currentKey.Package]; ok {
-			previousKey := previous.Key()
-			switch cmp := semver.Compare(currentKey.Revision, previousKey.Revision); {
-			case cmp == 0:
-				// Same revision.
-				klog.Warningf("Encountered package revisions whose versions compare equal: %q, %q", currentKey, previousKey)
-			case cmp < 0:
-				// currentKey.Revision < previousKey.Revision; no change
-			case cmp > 0:
-				// currentKey.Revision > previousKey.Revision; update latest
-				latest[currentKey.Package] = current
-			}
-		} else if semver.IsValid(currentKey.Revision) {
-			// First revision of the specific package; candidate for the latest.
-			latest[currentKey.Package] = current
-		}
-	}
-	// Mark the winners as latest
-	for _, v := range latest {
-		v.isLatestRevision = true
-	}
+func (r *cachedRepository) lock(f func()) {
+	r.mutex.Lock()
+	f()
+	r.mutex.Unlock()
 }
 
-func toPackageRevisionSlice(cached []*cachedPackageRevision, filter repository.ListPackageRevisionFilter) []repository.PackageRevision {
-	result := make([]repository.PackageRevision, 0, len(cached))
-	for _, p := range cached {
-		if filter.Matches(p) {
-			result = append(result, p)
-		}
-	}
-	sort.Slice(result, func(i, j int) bool {
-		ki, kl := result[i].Key(), result[j].Key()
-		switch res := strings.Compare(ki.Package, kl.Package); {
-		case res < 0:
-			return true
-		case res > 0:
-			return false
-		default:
-			// Equal. Compare next element
-		}
-		switch res := strings.Compare(ki.Revision, kl.Revision); {
-		case res < 0:
-			return true
-		case res > 0:
-			return false
-		default:
-			// Equal. Compare next element
-		}
-		switch res := strings.Compare(string(result[i].Lifecycle()), string(result[j].Lifecycle())); {
-		case res < 0:
-			return true
-		case res > 0:
-			return false
-		default:
-			// Equal. Compare next element
-		}
-
-		return strings.Compare(result[i].KubeObjectName(), result[j].KubeObjectName()) < 0
+func (r *cachedRepository) flush() {
+	r.lock(func() {
+		r.cachedPackageRevisions = nil
+		r.cachedPackages = nil
 	})
-	return result
 }
diff --git a/porch/pkg/cache/util.go b/porch/pkg/cache/util.go
new file mode 100644
index 00000000..5bc95359
--- /dev/null
+++ b/porch/pkg/cache/util.go
@@ -0,0 +1,136 @@
+// 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 cache
+
+import (
+	"sort"
+	"strings"
+
+	"github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1"
+	"github.com/GoogleContainerTools/kpt/porch/pkg/repository"
+	"golang.org/x/mod/semver"
+	"k8s.io/klog/v2"
+)
+
+func toCachedPackageRevisionSlice(revisions []repository.PackageRevision) []*cachedPackageRevision {
+	result := make([]*cachedPackageRevision, len(revisions))
+	for i := range revisions {
+		current := &cachedPackageRevision{
+			PackageRevision:  revisions[i],
+			isLatestRevision: false,
+		}
+		result[i] = current
+	}
+	identifyLatestRevisions(result)
+	return result
+}
+
+func toCachedPackageSlice(packages []repository.Package) []*cachedPackage {
+	result := make([]*cachedPackage, len(packages))
+	for i := range packages {
+		current := &cachedPackage{
+			Package:               packages[i],
+			latestPackageRevision: packages[i].GetLatestRevision(),
+		}
+		result[i] = current
+	}
+	return result
+}
+
+func identifyLatestRevisions(result []*cachedPackageRevision) {
+	// Compute the latest among the different revisions of the same package.
+	// The map is keyed by the package name; Values are the latest revision found so far.
+	latest := map[string]*cachedPackageRevision{}
+	for _, current := range result {
+		current.isLatestRevision = false // Clear all values
+
+		// Check if the current package revision is more recent than the one seen so far.
+		// Only consider Published packages
+		if current.Lifecycle() != v1alpha1.PackageRevisionLifecyclePublished {
+			continue
+		}
+
+		currentKey := current.Key()
+		if previous, ok := latest[currentKey.Package]; ok {
+			previousKey := previous.Key()
+			switch cmp := semver.Compare(currentKey.Revision, previousKey.Revision); {
+			case cmp == 0:
+				// Same revision.
+				klog.Warningf("Encountered package revisions whose versions compare equal: %q, %q", currentKey, previousKey)
+			case cmp < 0:
+				// currentKey.Revision < previousKey.Revision; no change
+			case cmp > 0:
+				// currentKey.Revision > previousKey.Revision; update latest
+				latest[currentKey.Package] = current
+			}
+		} else if semver.IsValid(currentKey.Revision) {
+			// First revision of the specific package; candidate for the latest.
+			latest[currentKey.Package] = current
+		}
+	}
+	// Mark the winners as latest
+	for _, v := range latest {
+		v.isLatestRevision = true
+	}
+}
+
+func toPackageRevisionSlice(cached []*cachedPackageRevision, filter repository.ListPackageRevisionFilter) []repository.PackageRevision {
+	result := make([]repository.PackageRevision, 0, len(cached))
+	for _, p := range cached {
+		if filter.Matches(p) {
+			result = append(result, p)
+		}
+	}
+	sort.Slice(result, func(i, j int) bool {
+		ki, kl := result[i].Key(), result[j].Key()
+		switch res := strings.Compare(ki.Package, kl.Package); {
+		case res < 0:
+			return true
+		case res > 0:
+			return false
+		default:
+			// Equal. Compare next element
+		}
+		switch res := strings.Compare(ki.Revision, kl.Revision); {
+		case res < 0:
+			return true
+		case res > 0:
+			return false
+		default:
+			// Equal. Compare next element
+		}
+		switch res := strings.Compare(string(result[i].Lifecycle()), string(result[j].Lifecycle())); {
+		case res < 0:
+			return true
+		case res > 0:
+			return false
+		default:
+			// Equal. Compare next element
+		}
+
+		return strings.Compare(result[i].KubeObjectName(), result[j].KubeObjectName()) < 0
+	})
+	return result
+}
+
+func toPackageSlice(cached []*cachedPackage, filter repository.ListPackageFilter) []repository.Package {
+	result := make([]repository.Package, 0, len(cached))
+	for _, p := range cached {
+		if filter.Matches(p) {
+			result = append(result, p)
+		}
+	}
+	return result
+}
diff --git a/porch/pkg/engine/clone.go b/porch/pkg/engine/clone.go
index 866b3e53..b04b749c 100644
--- a/porch/pkg/engine/clone.go
+++ b/porch/pkg/engine/clone.go
@@ -158,7 +158,7 @@ func (m *clonePackageMutation) cloneFromGit(ctx context.Context, gitPackage *api
 		return repository.PackageResources{}, fmt.Errorf("cannot clone Git repository: %w", err)
 	}
 
-	revision, lock, err := r.GetPackage(ctx, gitPackage.Ref, gitPackage.Directory)
+	revision, lock, err := r.GetPackageRevision(ctx, gitPackage.Ref, gitPackage.Directory)
 	if err != nil {
 		return repository.PackageResources{}, fmt.Errorf("cannot find package %s@%s: %w", gitPackage.Directory, gitPackage.Ref, err)
 	}
diff --git a/porch/pkg/engine/engine.go b/porch/pkg/engine/engine.go
index 144d09ed..ec9104f8 100644
--- a/porch/pkg/engine/engine.go
+++ b/porch/pkg/engine/engine.go
@@ -40,11 +40,16 @@ var tracer = otel.Tracer("engine")
 
 type CaDEngine interface {
 	OpenRepository(ctx context.Context, repositorySpec *configapi.Repository) (repository.Repository, error)
+	UpdatePackageResources(ctx context.Context, repositoryObj *configapi.Repository, oldPackage repository.PackageRevision, old, new *api.PackageRevisionResources) (repository.PackageRevision, error)
+	ListFunctions(ctx context.Context, repositoryObj *configapi.Repository) ([]repository.Function, error)
+
 	CreatePackageRevision(ctx context.Context, repositoryObj *configapi.Repository, obj *api.PackageRevision) (repository.PackageRevision, error)
 	UpdatePackageRevision(ctx context.Context, repositoryObj *configapi.Repository, oldPackage repository.PackageRevision, old, new *api.PackageRevision) (repository.PackageRevision, error)
-	UpdatePackageResources(ctx context.Context, repositoryObj *configapi.Repository, oldPackage repository.PackageRevision, old, new *api.PackageRevisionResources) (repository.PackageRevision, error)
 	DeletePackageRevision(ctx context.Context, repositoryObj *configapi.Repository, obj repository.PackageRevision) error
-	ListFunctions(ctx context.Context, repositoryObj *configapi.Repository) ([]repository.Function, error)
+
+	CreatePackage(ctx context.Context, repositoryObj *configapi.Repository, obj *api.Package) (repository.Package, error)
+	UpdatePackage(ctx context.Context, repositoryObj *configapi.Repository, oldPackage repository.Package, old, new *api.Package) (repository.Package, error)
+	DeletePackage(ctx context.Context, repositoryObj *configapi.Repository, obj repository.Package) error
 }
 
 func NewCaDEngine(opts ...EngineOption) (CaDEngine, error) {
@@ -306,7 +311,7 @@ func (cad *cadEngine) UpdatePackageRevision(ctx context.Context, repositoryObj *
 	// Re-render if we are making changes.
 	mutations = cad.conditionalAddRender(mutations)
 
-	draft, err := repo.UpdatePackage(ctx, oldPackage)
+	draft, err := repo.UpdatePackageRevision(ctx, oldPackage)
 	if err != nil {
 		return nil, err
 	}
@@ -370,6 +375,47 @@ func (cad *cadEngine) DeletePackageRevision(ctx context.Context, repositoryObj *
 	return nil
 }
 
+func (cad *cadEngine) CreatePackage(ctx context.Context, repositoryObj *configapi.Repository, obj *api.Package) (repository.Package, error) {
+	ctx, span := tracer.Start(ctx, "cadEngine::CreatePackage", trace.WithAttributes())
+	defer span.End()
+
+	repo, err := cad.cache.OpenRepository(ctx, repositoryObj)
+	if err != nil {
+		return nil, err
+	}
+	pkg, err := repo.CreatePackage(ctx, obj)
+	if err != nil {
+		return nil, err
+	}
+
+	return pkg, nil
+}
+
+func (cad *cadEngine) UpdatePackage(ctx context.Context, repositoryObj *configapi.Repository, oldPackage repository.Package, oldObj, newObj *api.Package) (repository.Package, error) {
+	ctx, span := tracer.Start(ctx, "cadEngine::UpdatePackage", trace.WithAttributes())
+	defer span.End()
+
+	// TODO
+	var pkg repository.Package
+	return pkg, fmt.Errorf("Updating packages is not yet supported")
+}
+
+func (cad *cadEngine) DeletePackage(ctx context.Context, repositoryObj *configapi.Repository, oldPackage repository.Package) error {
+	ctx, span := tracer.Start(ctx, "cadEngine::DeletePackage", trace.WithAttributes())
+	defer span.End()
+
+	repo, err := cad.cache.OpenRepository(ctx, repositoryObj)
+	if err != nil {
+		return err
+	}
+
+	if err := repo.DeletePackage(ctx, oldPackage); err != nil {
+		return err
+	}
+
+	return nil
+}
+
 func (cad *cadEngine) UpdatePackageResources(ctx context.Context, repositoryObj *configapi.Repository, oldPackage repository.PackageRevision, old, new *api.PackageRevisionResources) (repository.PackageRevision, error) {
 	ctx, span := tracer.Start(ctx, "cadEngine::UpdatePackageResources", trace.WithAttributes())
 	defer span.End()
@@ -392,7 +438,7 @@ func (cad *cadEngine) UpdatePackageResources(ctx context.Context, repositoryObj
 		return nil, err
 	}
 
-	draft, err := repo.UpdatePackage(ctx, oldPackage)
+	draft, err := repo.UpdatePackageRevision(ctx, oldPackage)
 	if err != nil {
 		return nil, err
 	}
diff --git a/porch/pkg/engine/fake/repository.go b/porch/pkg/engine/fake/repository.go
index 20d988d5..cb56d495 100644
--- a/porch/pkg/engine/fake/repository.go
+++ b/porch/pkg/engine/fake/repository.go
@@ -25,8 +25,11 @@ import (
 // TODO(mortent): Implement stub functionality for all functions from the interface.
 type Repository struct {
 	PackageRevisions []repository.PackageRevision
+	Packages         []repository.Package
 }
 
+var _ repository.Repository = &Repository{}
+
 func (r *Repository) ListPackageRevisions(context.Context, repository.ListPackageRevisionFilter) ([]repository.PackageRevision, error) {
 	return r.PackageRevisions, nil
 }
@@ -39,6 +42,18 @@ func (r *Repository) DeletePackageRevision(context.Context, repository.PackageRe
 	return nil
 }
 
-func (r *Repository) UpdatePackage(context.Context, repository.PackageRevision) (repository.PackageDraft, error) {
+func (r *Repository) UpdatePackageRevision(context.Context, repository.PackageRevision) (repository.PackageDraft, error) {
 	return nil, nil
 }
+
+func (r *Repository) ListPackages(context.Context, repository.ListPackageFilter) ([]repository.Package, error) {
+	return r.Packages, nil
+}
+
+func (r *Repository) CreatePackage(_ context.Context, pr *v1alpha1.Package) (repository.Package, error) {
+	return nil, nil
+}
+
+func (r *Repository) DeletePackage(_ context.Context, pr repository.Package) error {
+	return nil
+}
diff --git a/porch/pkg/git/git.go b/porch/pkg/git/git.go
index e93c3729..5b487d9d 100644
--- a/porch/pkg/git/git.go
+++ b/porch/pkg/git/git.go
@@ -50,7 +50,7 @@ const (
 
 type GitRepository interface {
 	repository.Repository
-	GetPackage(ctx context.Context, ref, path string) (repository.PackageRevision, kptfilev1.GitLock, error)
+	GetPackageRevision(ctx context.Context, ref, path string) (repository.PackageRevision, kptfilev1.GitLock, error)
 }
 
 //go:generate stringer -type=MainBranchStrategy -linecomment
@@ -160,6 +160,32 @@ type gitRepository struct {
 	mutex      sync.Mutex
 }
 
+var _ GitRepository = &gitRepository{}
+
+func (r *gitRepository) ListPackages(ctx context.Context, filter repository.ListPackageFilter) ([]repository.Package, error) {
+	ctx, span := tracer.Start(ctx, "gitRepository::ListPackages", trace.WithAttributes())
+	defer span.End()
+
+	// TODO
+	return nil, fmt.Errorf("ListPackages not yet supported for git repos")
+}
+
+func (r *gitRepository) CreatePackage(ctx context.Context, obj *v1alpha1.Package) (repository.Package, error) {
+	ctx, span := tracer.Start(ctx, "gitRepository::CreatePackage", trace.WithAttributes())
+	defer span.End()
+
+	// TODO: Create a 'Package' resource and an initial, empty 'PackageRevision'
+	return nil, fmt.Errorf("CreatePackage not yet supported for git repos")
+}
+
+func (r *gitRepository) DeletePackage(ctx context.Context, obj repository.Package) error {
+	ctx, span := tracer.Start(ctx, "gitRepository::DeletePackage", trace.WithAttributes())
+	defer span.End()
+
+	// TODO: Support package deletion using subresources (similar to the package revision approval flow)
+	return fmt.Errorf("DeletePackage not yet supported for git repos")
+}
+
 func (r *gitRepository) ListPackageRevisions(ctx context.Context, filter repository.ListPackageRevisionFilter) ([]repository.PackageRevision, error) {
 	ctx, span := tracer.Start(ctx, "gitRepository::ListPackageRevisions", trace.WithAttributes())
 	defer span.End()
@@ -257,6 +283,8 @@ func (r *gitRepository) CreatePackageRevision(ctx context.Context, obj *v1alpha1
 
 	draft := createDraftName(obj.Spec.PackageName, obj.Spec.Revision)
 
+	// TODO: This should also create a new 'Package' resource if one does not already exist
+
 	return &gitPackageDraft{
 		parent:    r,
 		path:      obj.Spec.PackageName,
@@ -270,8 +298,8 @@ func (r *gitRepository) CreatePackageRevision(ctx context.Context, obj *v1alpha1
 	}, nil
 }
 
-func (r *gitRepository) UpdatePackage(ctx context.Context, old repository.PackageRevision) (repository.PackageDraft, error) {
-	ctx, span := tracer.Start(ctx, "gitRepository::UpdatePackage", trace.WithAttributes())
+func (r *gitRepository) UpdatePackageRevision(ctx context.Context, old repository.PackageRevision) (repository.PackageDraft, error) {
+	ctx, span := tracer.Start(ctx, "gitRepository::UpdatePackageRevision", trace.WithAttributes())
 	defer span.End()
 
 	oldGitPackage, ok := old.(*gitPackageRevision)
@@ -321,7 +349,7 @@ func (r *gitRepository) DeletePackageRevision(ctx context.Context, old repositor
 
 	ref := oldGit.ref
 	if ref == nil {
-		// This is an internal error. In some rare cases (see GetPackage below) we create
+		// This is an internal error. In some rare cases (see GetPackageRevision below) we create
 		// package revisions without refs. They should never be returned via the API though.
 		return fmt.Errorf("cannot delete package with no ref: %s", oldGit.path)
 	}
@@ -367,18 +395,18 @@ func (r *gitRepository) DeletePackageRevision(ctx context.Context, old repositor
 	return nil
 }
 
-func (r *gitRepository) GetPackage(ctx context.Context, version, path string) (repository.PackageRevision, kptfilev1.GitLock, error) {
-	ctx, span := tracer.Start(ctx, "gitRepository::GetPackage", trace.WithAttributes())
+func (r *gitRepository) GetPackageRevision(ctx context.Context, version, path string) (repository.PackageRevision, kptfilev1.GitLock, error) {
+	ctx, span := tracer.Start(ctx, "gitRepository::GetPackageRevision", trace.WithAttributes())
 	defer span.End()
 
-	git := r.repo
+	gitRepo := r.repo
 
 	var hash plumbing.Hash
 
 	// Trim leading and trailing slashes
 	path = strings.Trim(path, "/")
 
-	// Versions map to git tags in one of two ways:
+	// Versions map to gitRepo tags in one of two ways:
 	//
 	// * directly (tag=version)- but then this means that all packages in the repo must be versioned together.
 	// * prefixed (tag=<packageDir/<version>) - solving the co-versioning problem.
@@ -395,7 +423,7 @@ func (r *gitRepository) GetPackage(ctx context.Context, version, path string) (r
 	refNames = append(refNames, "refs/remotes/origin/"+version)
 
 	for _, ref := range refNames {
-		if resolved, err := git.ResolveRevision(plumbing.Revision(ref)); err != nil {
+		if resolved, err := gitRepo.ResolveRevision(plumbing.Revision(ref)); err != nil {
 			if errors.Is(err, plumbing.ErrReferenceNotFound) {
 				continue
 			}
diff --git a/porch/pkg/git/package.go b/porch/pkg/git/package.go
index cd631365..c903919e 100644
--- a/porch/pkg/git/package.go
+++ b/porch/pkg/git/package.go
@@ -255,3 +255,5 @@ func (p *gitPackageRevision) Lifecycle() v1alpha1.PackageRevisionLifecycle {
 		return v1alpha1.PackageRevisionLifecyclePublished
 	}
 }
+
+// TODO: Define a type `gitPackage` to implement the Repository.Package interface
diff --git a/porch/pkg/oci/mutate.go b/porch/pkg/oci/mutate.go
index 6d240565..f574d6e6 100644
--- a/porch/pkg/oci/mutate.go
+++ b/porch/pkg/oci/mutate.go
@@ -62,7 +62,7 @@ func (r *ociRepository) CreatePackageRevision(ctx context.Context, obj *api.Pack
 	}, nil
 }
 
-func (r *ociRepository) UpdatePackage(ctx context.Context, old repository.PackageRevision) (repository.PackageDraft, error) {
+func (r *ociRepository) UpdatePackageRevision(ctx context.Context, old repository.PackageRevision) (repository.PackageDraft, error) {
 	oldPackage := old.(*ociPackageRevision)
 	packageName := oldPackage.packageName
 	revision := oldPackage.revision
@@ -293,3 +293,11 @@ func (r *ociRepository) DeletePackageRevision(ctx context.Context, old repositor
 
 	return nil
 }
+
+func (r *ociRepository) CreatePackage(ctx context.Context, obj *v1alpha1.Package) (repository.Package, error) {
+	return nil, fmt.Errorf("CreatePackage not supported for OCI packages")
+}
+
+func (r *ociRepository) DeletePackage(ctx context.Context, obj repository.Package) error {
+	return fmt.Errorf("DeletePackage not supported for OCI packages")
+}
diff --git a/porch/pkg/oci/oci.go b/porch/pkg/oci/oci.go
index 82dac03a..ca33d193 100644
--- a/porch/pkg/oci/oci.go
+++ b/porch/pkg/oci/oci.go
@@ -152,6 +152,10 @@ func (r *ociRepository) ListPackageRevisions(ctx context.Context, filter reposit
 	return result, nil
 }
 
+func (r *ociRepository) ListPackages(ctx context.Context, filter repository.ListPackageFilter) ([]repository.Package, error) {
+	return nil, fmt.Errorf("ListPackages not supported for OCI packages")
+}
+
 func (r *ociRepository) buildPackageRevision(ctx context.Context, name oci.ImageDigestName, packageName string, revision string, created time.Time) (repository.PackageRevision, error) {
 	if r.content != configapi.RepositoryContentPackage {
 		return nil, fmt.Errorf("repository is not a package repo, type is %v", r.content)
diff --git a/porch/pkg/registry/porch/fieldselector.go b/porch/pkg/registry/porch/fieldselector.go
index bc1fbd75..01abe3c6 100644
--- a/porch/pkg/registry/porch/fieldselector.go
+++ b/porch/pkg/registry/porch/fieldselector.go
@@ -23,6 +23,20 @@ import (
 	"k8s.io/apimachinery/pkg/selection"
 )
 
+// convertPackageRevisionFieldSelector is the schema conversion function for normalizing the the FieldSelector for PackageRevision
+func convertPackageFieldSelector(label, value string) (internalLabel, internalValue string, err error) {
+	switch label {
+	case "metadata.name":
+		return label, value, nil
+	case "metadata.namespace":
+		return label, value, nil
+	case "spec.packageName", "spec.repository":
+		return label, value, nil
+	default:
+		return "", "", fmt.Errorf("%q is not a known field selector", label)
+	}
+}
+
 // convertPackageRevisionFieldSelector is the schema conversion function for normalizing the the FieldSelector for PackageRevision
 func convertPackageRevisionFieldSelector(label, value string) (internalLabel, internalValue string, err error) {
 	switch label {
@@ -42,8 +56,8 @@ func convertPackageRevisionResourcesFieldSelector(label, value string) (internal
 	return convertPackageRevisionFieldSelector(label, value)
 }
 
-// packageFilter filters packages, extending repository.ListPackageRevisionFilter
-type packageFilter struct {
+// packageRevisionFilter filters packages, extending repository.ListPackageRevisionFilter
+type packageRevisionFilter struct {
 	repository.ListPackageRevisionFilter
 
 	// Namespace filters by the namespace of the objects
@@ -53,14 +67,65 @@ type packageFilter struct {
 	Repository string
 }
 
-// parsePackageRevisionFieldSelector parses client-provided fields.Selector into a packageFilter
-func parsePackageRevisionFieldSelector(fieldSelector fields.Selector) (packageFilter, error) {
+// packageFilter filters packages, extending repository.ListPackageFilter
+type packageFilter struct {
+	repository.ListPackageFilter
+
+	// Namespace filters by the namespace of the objects
+	Namespace string
+
+	// Repository restricts to repositories with the given name.
+	Repository string
+}
+
+// parsePackageFieldSelector parses client-provided fields.Selector into a packageFilter
+func parsePackageFieldSelector(fieldSelector fields.Selector) (packageFilter, error) {
 	var filter packageFilter
 
 	if fieldSelector == nil {
 		return filter, nil
 	}
 
+	requirements := fieldSelector.Requirements()
+	for _, requirement := range requirements {
+
+		switch requirement.Operator {
+		case selection.Equals, selection.DoesNotExist:
+			if requirement.Value == "" {
+				return filter, apierrors.NewBadRequest(fmt.Sprintf("unsupported fieldSelector value %q for field %q with operator %q", requirement.Value, requirement.Field, requirement.Operator))
+			}
+		default:
+			return filter, apierrors.NewBadRequest(fmt.Sprintf("unsupported fieldSelector operator %q for field %q", requirement.Operator, requirement.Field))
+		}
+
+		switch requirement.Field {
+		case "metadata.name":
+			filter.KubeObjectName = requirement.Value
+
+		case "metadata.namespace":
+			filter.Namespace = requirement.Value
+
+		case "spec.packageName":
+			filter.Package = requirement.Value
+		case "spec.repository":
+			filter.Repository = requirement.Value
+
+		default:
+			return filter, apierrors.NewBadRequest(fmt.Sprintf("unknown fieldSelector field %q", requirement.Field))
+		}
+	}
+
+	return filter, nil
+}
+
+// parsePackageRevisionFieldSelector parses client-provided fields.Selector into a packageRevisionFilter
+func parsePackageRevisionFieldSelector(fieldSelector fields.Selector) (packageRevisionFilter, error) {
+	var filter packageRevisionFilter
+
+	if fieldSelector == nil {
+		return filter, nil
+	}
+
 	requirements := fieldSelector.Requirements()
 	for _, requirement := range requirements {
 
@@ -95,8 +160,8 @@ func parsePackageRevisionFieldSelector(fieldSelector fields.Selector) (packageFi
 	return filter, nil
 }
 
-// parsePackageRevisionResourcesFieldSelector parses client-provided fields.Selector into a packageFilter
-func parsePackageRevisionResourcesFieldSelector(fieldSelector fields.Selector) (packageFilter, error) {
+// parsePackageRevisionResourcesFieldSelector parses client-provided fields.Selector into a packageRevisionFilter
+func parsePackageRevisionResourcesFieldSelector(fieldSelector fields.Selector) (packageRevisionFilter, error) {
 	// TOOD: This is a little weird, because we don't have the same fields on PackageRevisionResources.
 	// But we probably should have the key fields
 	return parsePackageRevisionFieldSelector(fieldSelector)
diff --git a/porch/pkg/registry/porch/functions.go b/porch/pkg/registry/porch/functions.go
index e97a6b3b..5a18eb51 100644
--- a/porch/pkg/registry/porch/functions.go
+++ b/porch/pkg/registry/porch/functions.go
@@ -26,6 +26,7 @@ import (
 	metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apiserver/pkg/endpoints/request"
 	"k8s.io/apiserver/pkg/registry/rest"
 	"sigs.k8s.io/controller-runtime/pkg/client"
@@ -96,7 +97,7 @@ func (f *functions) Get(ctx context.Context, name string, options *metav1.GetOpt
 	}
 
 	if fn, err := parseFunctionName(name); err != nil {
-		return nil, apierrors.NewNotFound(v1alpha1.FunctionGVR.GroupResource(), name)
+		return nil, apierrors.NewNotFound(schema.GroupResource(v1alpha1.FunctionGVR.GroupResource()), name)
 	} else {
 		repositoryKey.Name = fn.repository
 	}
@@ -104,7 +105,7 @@ func (f *functions) Get(ctx context.Context, name string, options *metav1.GetOpt
 	var repository configapi.Repository
 	if err := f.coreClient.Get(ctx, repositoryKey, &repository); err != nil {
 		if apierrors.IsNotFound(err) {
-			return nil, apierrors.NewNotFound(v1alpha1.FunctionGVR.GroupResource(), name)
+			return nil, apierrors.NewNotFound(schema.GroupResource(v1alpha1.FunctionGVR.GroupResource()), name)
 		}
 		return nil, fmt.Errorf("error getting repository %s: %w", repositoryKey, err)
 	}
@@ -121,7 +122,7 @@ func (f *functions) Get(ctx context.Context, name string, options *metav1.GetOpt
 		}
 	}
 
-	return nil, apierrors.NewNotFound(v1alpha1.FunctionGVR.GroupResource(), name)
+	return nil, apierrors.NewNotFound(schema.GroupResource(v1alpha1.FunctionGVR.GroupResource()), name)
 }
 
 type functionName struct {
diff --git a/porch/pkg/registry/porch/package.go b/porch/pkg/registry/porch/package.go
new file mode 100644
index 00000000..f6002131
--- /dev/null
+++ b/porch/pkg/registry/porch/package.go
@@ -0,0 +1,197 @@
+// 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 porch
+
+import (
+	"context"
+	"fmt"
+
+	api "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1"
+	"github.com/GoogleContainerTools/kpt/porch/pkg/repository"
+	"go.opentelemetry.io/otel/trace"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
+	"k8s.io/apiserver/pkg/registry/rest"
+	"k8s.io/klog/v2"
+)
+
+type packages struct {
+	packageCommon
+	rest.TableConvertor
+}
+
+var _ rest.Storage = &packages{}
+var _ rest.Lister = &packages{}
+var _ rest.Getter = &packages{}
+var _ rest.Scoper = &packages{}
+var _ rest.Creater = &packages{}
+var _ rest.Updater = &packages{}
+var _ rest.GracefulDeleter = &packages{}
+
+func (r *packages) New() runtime.Object {
+	return &api.Package{}
+}
+
+func (r *packages) NewList() runtime.Object {
+	return &api.PackageList{}
+}
+
+func (r *packages) NamespaceScoped() bool {
+	return true
+}
+
+// List selects resources in the storage which match to the selector. 'options' can be nil.
+func (r *packages) List(ctx context.Context, options *metainternalversion.ListOptions) (runtime.Object, error) {
+	ctx, span := tracer.Start(ctx, "packages::List", trace.WithAttributes())
+	defer span.End()
+
+	result := &api.PackageList{
+		TypeMeta: metav1.TypeMeta{
+			Kind:       "PackageList",
+			APIVersion: api.SchemeGroupVersion.Identifier(),
+		},
+	}
+
+	filter, err := parsePackageFieldSelector(options.FieldSelector)
+	if err != nil {
+		return nil, err
+	}
+
+	if err := r.packageCommon.listPackages(ctx, filter, func(p repository.Package) error {
+		item := p.GetPackage()
+		result.Items = append(result.Items, *item)
+		return nil
+	}); err != nil {
+		return nil, err
+	}
+
+	return result, nil
+}
+
+// Get implements the Getter interface
+func (r *packages) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
+	ctx, span := tracer.Start(ctx, "packages::Get", trace.WithAttributes())
+	defer span.End()
+
+	pkg, err := r.getPackage(ctx, name)
+	if err != nil {
+		return nil, err
+	}
+
+	obj := pkg.GetPackage()
+	return obj, nil
+}
+
+// Create implements the Creater interface.
+func (r *packages) Create(ctx context.Context, runtimeObject runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
+	ctx, span := tracer.Start(ctx, "packages::Create", trace.WithAttributes())
+	defer span.End()
+
+	ns, namespaced := genericapirequest.NamespaceFrom(ctx)
+	if !namespaced {
+		return nil, apierrors.NewBadRequest("namespace must be specified")
+	}
+
+	obj, ok := runtimeObject.(*api.Package)
+	if !ok {
+		return nil, apierrors.NewBadRequest(fmt.Sprintf("expected Package object, got %T", runtimeObject))
+	}
+
+	// TODO: Accpept some form of client-provided name, for example using GenerateName
+	// and figure out where we can store it (in Kptfile?). Porch can then append unique
+	// suffix to the names while respecting client-provided value as well.
+	if obj.Name != "" {
+		klog.Warningf("Client provided metadata.name %q", obj.Name)
+	}
+
+	repositoryName := obj.Spec.RepositoryName
+	if repositoryName == "" {
+		return nil, apierrors.NewBadRequest("spec.repository is required")
+	}
+
+	repositoryObj, err := r.packageCommon.getRepositoryObj(ctx, ns, repositoryName)
+	if err != nil {
+		return nil, err
+	}
+
+	fieldErrors := r.createStrategy.Validate(ctx, runtimeObject)
+	if len(fieldErrors) > 0 {
+		return nil, apierrors.NewInvalid(api.SchemeGroupVersion.WithKind("Package").GroupKind(), obj.Name, fieldErrors)
+	}
+
+	rev, err := r.cad.CreatePackage(ctx, repositoryObj, obj)
+	if err != nil {
+		return nil, apierrors.NewInternalError(err)
+	}
+
+	created := rev.GetPackage()
+	return created, nil
+}
+
+// Update implements the Updater interface.
+
+// Update finds a resource in the storage and updates it. Some implementations
+// may allow updates creates the object - they should set the created boolean
+// to true.
+func (r *packages) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
+	ctx, span := tracer.Start(ctx, "packages::Update", trace.WithAttributes())
+	defer span.End()
+
+	return r.packageCommon.updatePackage(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options)
+}
+
+// Delete implements the GracefulDeleter interface.
+// Delete finds a resource in the storage and deletes it.
+// The delete attempt is validated by the deleteValidation first.
+// If options are provided, the resource will attempt to honor them or return an invalid
+// request error.
+// Although it can return an arbitrary error value, IsNotFound(err) is true for the
+// returned error value err when the specified resource is not found.
+// Delete *may* return the object that was deleted, or a status object indicating additional
+// information about deletion.
+// It also returns a boolean which is set to true if the resource was instantly
+// deleted or false if it will be deleted asynchronously.
+func (r *packages) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
+	ctx, span := tracer.Start(ctx, "packages::Delete", trace.WithAttributes())
+	defer span.End()
+
+	// TODO: Verify options are empty?
+
+	ns, namespaced := genericapirequest.NamespaceFrom(ctx)
+	if !namespaced {
+		return nil, false, apierrors.NewBadRequest("namespace must be specified")
+	}
+
+	oldPackage, err := r.packageCommon.getPackage(ctx, name)
+	if err != nil {
+		return nil, false, err
+	}
+
+	oldObj := oldPackage.GetPackage()
+	repositoryObj, err := r.packageCommon.validateDelete(ctx, deleteValidation, oldObj, name, ns)
+	if err != nil {
+		return nil, false, err
+	}
+
+	if err := r.cad.DeletePackage(ctx, repositoryObj, oldPackage); err != nil {
+		return nil, false, apierrors.NewInternalError(err)
+	}
+
+	// TODO: Should we do an async delete?
+	return oldObj, true, nil
+}
diff --git a/porch/pkg/registry/porch/packagecommon.go b/porch/pkg/registry/porch/packagecommon.go
index f9a59cba..550e815f 100644
--- a/porch/pkg/registry/porch/packagecommon.go
+++ b/porch/pkg/registry/porch/packagecommon.go
@@ -46,7 +46,7 @@ type packageCommon struct {
 	createStrategy SimpleRESTCreateStrategy
 }
 
-func (r *packageCommon) listPackages(ctx context.Context, filter packageFilter, callback func(p repository.PackageRevision) error) error {
+func (r *packageCommon) listPackageRevisions(ctx context.Context, filter packageRevisionFilter, callback func(p repository.PackageRevision) error) error {
 	var opts []client.ListOption
 	if ns, namespaced := genericapirequest.NamespaceFrom(ctx); namespaced {
 		opts = append(opts, client.InNamespace(ns))
@@ -87,12 +87,52 @@ func (r *packageCommon) listPackages(ctx context.Context, filter packageFilter,
 	return nil
 }
 
-func (r *packageCommon) getPackage(ctx context.Context, name string) (repository.PackageRevision, error) {
+func (r *packageCommon) listPackages(ctx context.Context, filter packageFilter, callback func(p repository.Package) error) error {
+	var opts []client.ListOption
+	if ns, namespaced := genericapirequest.NamespaceFrom(ctx); namespaced {
+		opts = append(opts, client.InNamespace(ns))
+
+		if filter.Namespace != "" && ns != filter.Namespace {
+			return fmt.Errorf("conflicting namespaces specified: %q and %q", ns, filter.Namespace)
+		}
+	}
+
+	// TODO: Filter on filter.Repository?
+	var repositories configapi.RepositoryList
+	if err := r.coreClient.List(ctx, &repositories, opts...); err != nil {
+		return fmt.Errorf("error listing repository objects: %w", err)
+	}
+
+	for i := range repositories.Items {
+		repositoryObj := &repositories.Items[i]
+
+		if filter.Repository != "" && filter.Repository != repositoryObj.GetName() {
+			continue
+		}
+
+		repository, err := r.cad.OpenRepository(ctx, repositoryObj)
+		if err != nil {
+			return err
+		}
+
+		revisions, err := repository.ListPackages(ctx, filter.ListPackageFilter)
+		if err != nil {
+			return err
+		}
+		for _, rev := range revisions {
+			if err := callback(rev); err != nil {
+				return err
+			}
+		}
+	}
+	return nil
+}
+
+func (r *packageCommon) openRepository(ctx context.Context, name string) (repository.Repository, error) {
 	ns, namespaced := genericapirequest.NamespaceFrom(ctx)
 	if !namespaced {
 		return nil, fmt.Errorf("namespace must be specified")
 	}
-
 	repositoryName, err := ParseRepositoryName(name)
 	if err != nil {
 		return nil, apierrors.NewNotFound(r.gr, name)
@@ -106,12 +146,26 @@ func (r *packageCommon) getPackage(ctx context.Context, name string) (repository
 		}
 		return nil, fmt.Errorf("error getting repository %v: %w", repositoryID, err)
 	}
+	return r.cad.OpenRepository(ctx, &repositoryObj)
+}
 
-	repo, err := r.cad.OpenRepository(ctx, &repositoryObj)
+func (r *packageCommon) getRepositoryObj(ctx context.Context, ns string, repositoryName string) (*configapi.Repository, error) {
+	var repositoryObj configapi.Repository
+	repositoryID := types.NamespacedName{Namespace: ns, Name: repositoryName}
+	if err := r.coreClient.Get(ctx, repositoryID, &repositoryObj); err != nil {
+		if apierrors.IsNotFound(err) {
+			return nil, apierrors.NewNotFound(configapi.KindRepository.GroupResource(), repositoryID.Name)
+		}
+		return nil, apierrors.NewInternalError(fmt.Errorf("error getting repository %v: %w", repositoryID, err))
+	}
+	return &repositoryObj, nil
+}
+
+func (r *packageCommon) getPackageRevision(ctx context.Context, name string) (repository.PackageRevision, error) {
+	repo, err := r.openRepository(ctx, name)
 	if err != nil {
 		return nil, err
 	}
-
 	revisions, err := repo.ListPackageRevisions(ctx, repository.ListPackageRevisionFilter{KubeObjectName: name})
 	if err != nil {
 		return nil, err
@@ -125,14 +179,23 @@ func (r *packageCommon) getPackage(ctx context.Context, name string) (repository
 	return nil, apierrors.NewNotFound(r.gr, name)
 }
 
-func (r *packageCommon) getPackageRevision(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
-	pkg, err := r.getPackage(ctx, name)
+func (r *packageCommon) getPackage(ctx context.Context, name string) (repository.Package, error) {
+	repo, err := r.openRepository(ctx, name)
 	if err != nil {
 		return nil, err
 	}
 
-	obj := pkg.GetPackageRevision()
-	return obj, nil
+	revisions, err := repo.ListPackages(ctx, repository.ListPackageFilter{KubeObjectName: name})
+	if err != nil {
+		return nil, err
+	}
+	for _, rev := range revisions {
+		if rev.KubeObjectName() == name {
+			return rev, nil
+		}
+	}
+
+	return nil, apierrors.NewNotFound(r.gr, name)
 }
 
 // Common implementation of PackageRevision update logic.
@@ -147,7 +210,7 @@ func (r *packageCommon) updatePackageRevision(ctx context.Context, name string,
 	// isCreate tracks whether this is an update that creates an object (this happens in server-side apply)
 	isCreate := false
 
-	oldPackage, err := r.getPackage(ctx, name)
+	oldPackage, err := r.getPackageRevision(ctx, name)
 	if err != nil {
 		if forceAllowCreate && apierrors.IsNotFound(err) {
 			// For server-side apply, we can create the object here
@@ -179,43 +242,109 @@ func (r *packageCommon) updatePackageRevision(ctx context.Context, name string,
 		newRuntimeObj = typed
 	}
 
-	r.updateStrategy.PrepareForUpdate(ctx, newRuntimeObj, oldRuntimeObj)
+	if err := r.validateUpdate(ctx, newRuntimeObj, oldRuntimeObj, isCreate, createValidation,
+		updateValidation, "PackageRevision", name); err != nil {
+		return nil, false, err
+	}
+
+	newObj, ok := newRuntimeObj.(*api.PackageRevision)
+	if !ok {
+		return nil, false, apierrors.NewBadRequest(fmt.Sprintf("expected PackageRevision object, got %T", newRuntimeObj))
+	}
+
+	repositoryName, err := ParseRepositoryName(name)
+	if err != nil {
+		return nil, false, apierrors.NewBadRequest(fmt.Sprintf("invalid name %q", name))
+	}
+	if isCreate {
+		repositoryName = newObj.Spec.RepositoryName
+		if repositoryName == "" {
+			return nil, false, apierrors.NewBadRequest(fmt.Sprintf("invalid repositoryName %q", name))
+		}
+	}
+
+	var repositoryObj configapi.Repository
+	repositoryID := types.NamespacedName{Namespace: ns, Name: repositoryName}
+	if err := r.coreClient.Get(ctx, repositoryID, &repositoryObj); err != nil {
+		if apierrors.IsNotFound(err) {
+			return nil, false, apierrors.NewNotFound(configapi.KindRepository.GroupResource(), repositoryID.Name)
+		}
+		return nil, false, apierrors.NewInternalError(fmt.Errorf("error getting repository %v: %w", repositoryID, err))
+	}
 
 	if !isCreate {
-		if updateValidation != nil {
-			err := updateValidation(ctx, newRuntimeObj, oldRuntimeObj)
-			if err != nil {
-				klog.Infof("update failed validation: %v", err)
-				return nil, false, err
-			}
+		rev, err := r.cad.UpdatePackageRevision(ctx, &repositoryObj, oldPackage, oldRuntimeObj.(*api.PackageRevision), newObj)
+		if err != nil {
+			return nil, false, apierrors.NewInternalError(err)
 		}
 
-		fieldErrors := r.updateStrategy.ValidateUpdate(ctx, newRuntimeObj, oldRuntimeObj)
-		if len(fieldErrors) > 0 {
-			return nil, false, apierrors.NewInvalid(api.SchemeGroupVersion.WithKind("PackageRevision").GroupKind(), name, fieldErrors)
+		updated := rev.GetPackageRevision()
+
+		return updated, false, nil
+	} else {
+		rev, err := r.cad.CreatePackageRevision(ctx, &repositoryObj, newObj)
+		if err != nil {
+			klog.Infof("error creating package: %v", err)
+			return nil, false, apierrors.NewInternalError(err)
 		}
+
+		created := rev.GetPackageRevision()
+		return created, true, nil
 	}
+}
 
-	if isCreate {
-		if createValidation != nil {
-			err := createValidation(ctx, newRuntimeObj)
-			if err != nil {
-				klog.Infof("update failed create validation: %v", err)
-				return nil, false, err
-			}
+// Common implementation of Package update logic.
+func (r *packageCommon) updatePackage(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
+	// TODO: Is this all boilerplate??
+
+	ns, namespaced := genericapirequest.NamespaceFrom(ctx)
+	if !namespaced {
+		return nil, false, apierrors.NewBadRequest("namespace must be specified")
+	}
+
+	// isCreate tracks whether this is an update that creates an object (this happens in server-side apply)
+	isCreate := false
+
+	oldPackage, err := r.getPackage(ctx, name)
+	if err != nil {
+		if forceAllowCreate && apierrors.IsNotFound(err) {
+			// For server-side apply, we can create the object here
+			isCreate = true
+		} else {
+			return nil, false, err
 		}
+	}
 
-		fieldErrors := r.createStrategy.Validate(ctx, newRuntimeObj)
-		if len(fieldErrors) > 0 {
-			return nil, false, apierrors.NewInvalid(api.SchemeGroupVersion.WithKind("PackageRevision").GroupKind(), name, fieldErrors)
+	var oldRuntimeObj 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 {
+		oldRuntimeObj = oldPackage.GetPackage()
+	}
+
+	newRuntimeObj, err := objInfo.UpdatedObject(ctx, oldRuntimeObj)
+	if err != nil {
+		klog.Infof("update failed to construct UpdatedObject: %v", err)
+		return nil, false, err
+	}
+
+	// This type conversion is necessary because mutations work with unversioned types
+	// (mostly for historical reasons).  So the server-side-apply library returns an unversioned object.
+	if unversioned, isUnversioned := newRuntimeObj.(*unversionedapi.PackageRevision); isUnversioned {
+		klog.Warningf("converting from unversioned to versioned object")
+		typed := &api.PackageRevision{}
+		if err := r.scheme.Convert(unversioned, typed, nil); err != nil {
+			return nil, false, fmt.Errorf("failed to convert %T to %T: %w", unversioned, typed, err)
 		}
+		newRuntimeObj = typed
 	}
 
-	r.updateStrategy.Canonicalize(newRuntimeObj)
+	if err := r.validateUpdate(ctx, newRuntimeObj, oldRuntimeObj, isCreate, createValidation,
+		updateValidation, "Package", name); err != nil {
+		return nil, false, err
+	}
 
-	newObj, ok := newRuntimeObj.(*api.PackageRevision)
+	newObj, ok := newRuntimeObj.(*api.Package)
 	if !ok {
-		return nil, false, apierrors.NewBadRequest(fmt.Sprintf("expected PackageRevision object, got %T", newRuntimeObj))
+		return nil, false, apierrors.NewBadRequest(fmt.Sprintf("expected Package object, got %T", newRuntimeObj))
 	}
 
 	repositoryName, err := ParseRepositoryName(name)
@@ -239,22 +368,80 @@ func (r *packageCommon) updatePackageRevision(ctx context.Context, name string,
 	}
 
 	if !isCreate {
-		rev, err := r.cad.UpdatePackageRevision(ctx, &repositoryObj, oldPackage, oldRuntimeObj.(*api.PackageRevision), newObj)
+		rev, err := r.cad.UpdatePackage(ctx, &repositoryObj, oldPackage, oldRuntimeObj.(*api.Package), newObj)
 		if err != nil {
 			return nil, false, apierrors.NewInternalError(err)
 		}
 
-		updated := rev.GetPackageRevision()
+		updated := rev.GetPackage()
 
 		return updated, false, nil
 	} else {
-		rev, err := r.cad.CreatePackageRevision(ctx, &repositoryObj, newObj)
+		rev, err := r.cad.CreatePackage(ctx, &repositoryObj, newObj)
 		if err != nil {
 			klog.Infof("error creating package: %v", err)
 			return nil, false, apierrors.NewInternalError(err)
 		}
 
-		created := rev.GetPackageRevision()
+		created := rev.GetPackage()
 		return created, true, nil
 	}
 }
+
+func (r *packageCommon) validateDelete(ctx context.Context, deleteValidation rest.ValidateObjectFunc, obj runtime.Object,
+	repoName string, ns string) (*configapi.Repository, error) {
+	if deleteValidation != nil {
+		err := deleteValidation(ctx, obj)
+		if err != nil {
+			klog.Infof("delete failed validation: %v", err)
+			return nil, err
+		}
+	}
+	repositoryName, err := ParseRepositoryName(repoName)
+	if err != nil {
+		return nil, apierrors.NewBadRequest(fmt.Sprintf("invalid name %q", repoName))
+	}
+	repositoryObj, err := r.getRepositoryObj(ctx, ns, repositoryName)
+	if err != nil {
+		return nil, err
+	}
+	return repositoryObj, nil
+}
+
+func (r *packageCommon) validateUpdate(ctx context.Context, newRuntimeObj runtime.Object, oldRuntimeObj runtime.Object, create bool,
+	createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, kind string, name string) error {
+	r.updateStrategy.PrepareForUpdate(ctx, newRuntimeObj, oldRuntimeObj)
+
+	if create {
+		if createValidation != nil {
+			err := createValidation(ctx, newRuntimeObj)
+			if err != nil {
+				klog.Infof("update failed create validation: %v", err)
+				return err
+			}
+		}
+
+		fieldErrors := r.createStrategy.Validate(ctx, newRuntimeObj)
+		if len(fieldErrors) > 0 {
+			return apierrors.NewInvalid(api.SchemeGroupVersion.WithKind(kind).GroupKind(), name, fieldErrors)
+		}
+	}
+
+	if !create {
+		if updateValidation != nil {
+			err := updateValidation(ctx, newRuntimeObj, oldRuntimeObj)
+			if err != nil {
+				klog.Infof("update failed validation: %v", err)
+				return err
+			}
+		}
+
+		fieldErrors := r.updateStrategy.ValidateUpdate(ctx, newRuntimeObj, oldRuntimeObj)
+		if len(fieldErrors) > 0 {
+			return apierrors.NewInvalid(api.SchemeGroupVersion.WithKind(kind).GroupKind(), name, fieldErrors)
+		}
+	}
+
+	r.updateStrategy.Canonicalize(newRuntimeObj)
+	return nil
+}
diff --git a/porch/pkg/registry/porch/packagerevision.go b/porch/pkg/registry/porch/packagerevision.go
index 1f9b2e17..42cabc36 100644
--- a/porch/pkg/registry/porch/packagerevision.go
+++ b/porch/pkg/registry/porch/packagerevision.go
@@ -17,10 +17,8 @@ package porch
 import (
 	"context"
 	"fmt"
-	"strings"
 
 	api "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1"
-	configapi "github.com/GoogleContainerTools/kpt/porch/api/porchconfig/v1alpha1"
 	"github.com/GoogleContainerTools/kpt/porch/pkg/repository"
 	"go.opentelemetry.io/otel"
 	"go.opentelemetry.io/otel/trace"
@@ -28,8 +26,6 @@ import (
 	metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
-	"k8s.io/apimachinery/pkg/types"
-	"k8s.io/apimachinery/pkg/util/validation/field"
 	genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
 	"k8s.io/apiserver/pkg/registry/rest"
 	"k8s.io/klog/v2"
@@ -79,7 +75,7 @@ func (r *packageRevisions) List(ctx context.Context, options *metainternalversio
 		return nil, err
 	}
 
-	if err := r.packageCommon.listPackages(ctx, filter, func(p repository.PackageRevision) error {
+	if err := r.packageCommon.listPackageRevisions(ctx, filter, func(p repository.PackageRevision) error {
 		item := p.GetPackageRevision()
 		result.Items = append(result.Items, *item)
 		return nil
@@ -95,7 +91,13 @@ func (r *packageRevisions) Get(ctx context.Context, name string, options *metav1
 	ctx, span := tracer.Start(ctx, "packageRevisions::Get", trace.WithAttributes())
 	defer span.End()
 
-	return r.packageCommon.getPackageRevision(ctx, name, options)
+	pkg, err := r.getPackageRevision(ctx, name)
+	if err != nil {
+		return nil, err
+	}
+
+	obj := pkg.GetPackageRevision()
+	return obj, nil
 }
 
 // Create implements the Creater interface.
@@ -125,13 +127,9 @@ func (r *packageRevisions) Create(ctx context.Context, runtimeObject runtime.Obj
 		return nil, apierrors.NewBadRequest("spec.repositoryName is required")
 	}
 
-	var repositoryObj configapi.Repository
-	repositoryID := types.NamespacedName{Namespace: ns, Name: repositoryName}
-	if err := r.coreClient.Get(ctx, repositoryID, &repositoryObj); err != nil {
-		if apierrors.IsNotFound(err) {
-			return nil, apierrors.NewNotFound(configapi.KindRepository.GroupResource(), repositoryID.Name)
-		}
-		return nil, apierrors.NewInternalError(fmt.Errorf("error getting repository %v: %w", repositoryID, err))
+	repositoryObj, err := r.packageCommon.getRepositoryObj(ctx, ns, repositoryName)
+	if err != nil {
+		return nil, err
 	}
 
 	fieldErrors := r.createStrategy.Validate(ctx, runtimeObject)
@@ -139,7 +137,7 @@ func (r *packageRevisions) Create(ctx context.Context, runtimeObject runtime.Obj
 		return nil, apierrors.NewInvalid(api.SchemeGroupVersion.WithKind("PackageRevision").GroupKind(), obj.Name, fieldErrors)
 	}
 
-	rev, err := r.cad.CreatePackageRevision(ctx, &repositoryObj, obj)
+	rev, err := r.cad.CreatePackageRevision(ctx, repositoryObj, obj)
 	if err != nil {
 		return nil, apierrors.NewInternalError(err)
 	}
@@ -180,119 +178,21 @@ func (r *packageRevisions) Delete(ctx context.Context, name string, deleteValida
 		return nil, false, apierrors.NewBadRequest("namespace must be specified")
 	}
 
-	oldPackage, err := r.packageCommon.getPackage(ctx, name)
+	oldPackage, err := r.packageCommon.getPackageRevision(ctx, name)
 	if err != nil {
 		return nil, false, err
 	}
 
 	oldObj := oldPackage.GetPackageRevision()
-
-	if deleteValidation != nil {
-		err := deleteValidation(ctx, oldObj)
-		if err != nil {
-			klog.Infof("delete failed validation: %v", err)
-			return nil, false, err
-		}
-	}
-
-	// TODO: Verify options are empty?
-
-	repositoryName, err := ParseRepositoryName(name)
+	repositoryObj, err := r.packageCommon.validateDelete(ctx, deleteValidation, oldObj, name, ns)
 	if err != nil {
-		return nil, false, apierrors.NewBadRequest(fmt.Sprintf("invalid name %q", name))
-	}
-
-	var repositoryObj configapi.Repository
-	repositoryID := types.NamespacedName{Namespace: ns, Name: repositoryName}
-	if err := r.coreClient.Get(ctx, repositoryID, &repositoryObj); err != nil {
-		if apierrors.IsNotFound(err) {
-			return nil, false, apierrors.NewNotFound(configapi.KindRepository.GroupResource(), repositoryID.Name)
-		}
-		return nil, false, apierrors.NewInternalError(fmt.Errorf("error getting repository %v: %w", repositoryID, err))
+		return nil, false, err
 	}
 
-	if err := r.cad.DeletePackageRevision(ctx, &repositoryObj, oldPackage); err != nil {
+	if err := r.cad.DeletePackageRevision(ctx, repositoryObj, oldPackage); err != nil {
 		return nil, false, apierrors.NewInternalError(err)
 	}
 
 	// TODO: Should we do an async delete?
 	return oldObj, true, nil
 }
-
-// PackageRevisions Update Strategy
-
-type packageRevisionStrategy struct{}
-
-var _ SimpleRESTUpdateStrategy = packageRevisionStrategy{}
-
-func (s packageRevisionStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
-}
-
-func (s packageRevisionStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
-	allErrs := field.ErrorList{}
-	oldRevision := old.(*api.PackageRevision)
-	newRevision := obj.(*api.PackageRevision)
-
-	switch lifecycle := oldRevision.Spec.Lifecycle; lifecycle {
-	case "", api.PackageRevisionLifecycleDraft, api.PackageRevisionLifecycleProposed:
-		// valid
-
-	default:
-		allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "lifecycle"), lifecycle, fmt.Sprintf("can only update package with lifecycle value one of %s",
-			strings.Join([]string{
-				string(api.PackageRevisionLifecycleDraft),
-				string(api.PackageRevisionLifecycleProposed),
-			}, ",")),
-		))
-
-	}
-
-	switch lifecycle := newRevision.Spec.Lifecycle; lifecycle {
-	case "", api.PackageRevisionLifecycleDraft, api.PackageRevisionLifecycleProposed:
-		// valid
-
-	default:
-		allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "lifecycle"), lifecycle, fmt.Sprintf("value can be only updated to %s",
-			strings.Join([]string{
-				string(api.PackageRevisionLifecycleDraft),
-				string(api.PackageRevisionLifecycleProposed),
-			}, ",")),
-		))
-	}
-
-	return allErrs
-}
-
-func (s packageRevisionStrategy) Canonicalize(obj runtime.Object) {
-	pr := obj.(*api.PackageRevision)
-	if pr.Spec.Lifecycle == "" {
-		// Set default
-		pr.Spec.Lifecycle = api.PackageRevisionLifecycleDraft
-	}
-}
-
-var _ SimpleRESTCreateStrategy = packageRevisionStrategy{}
-
-// Validate returns an ErrorList with validation errors or nil.  Validate
-// is invoked after default fields in the object have been filled in
-// before the object is persisted.  This method should not mutate the
-// object.
-func (s packageRevisionStrategy) Validate(ctx context.Context, runtimeObj runtime.Object) field.ErrorList {
-	allErrs := field.ErrorList{}
-
-	obj := runtimeObj.(*api.PackageRevision)
-
-	switch lifecycle := obj.Spec.Lifecycle; lifecycle {
-	case "", api.PackageRevisionLifecycleDraft:
-		// valid
-
-	default:
-		allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "lifecycle"), lifecycle, fmt.Sprintf("value can be only created as %s",
-			strings.Join([]string{
-				string(api.PackageRevisionLifecycleDraft),
-			}, ",")),
-		))
-	}
-
-	return allErrs
-}
diff --git a/porch/pkg/registry/porch/packagerevisionresources.go b/porch/pkg/registry/porch/packagerevisionresources.go
index 7605b55b..bd89e556 100644
--- a/porch/pkg/registry/porch/packagerevisionresources.go
+++ b/porch/pkg/registry/porch/packagerevisionresources.go
@@ -17,6 +17,7 @@ package porch
 import (
 	"context"
 	"fmt"
+	"k8s.io/apimachinery/pkg/runtime/schema"
 
 	api "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1"
 	"github.com/GoogleContainerTools/kpt/porch/api/porchconfig/v1alpha1"
@@ -73,7 +74,7 @@ func (r *packageRevisionResources) List(ctx context.Context, options *metaintern
 		return nil, err
 	}
 
-	if err := r.packageCommon.listPackages(ctx, filter, func(p repository.PackageRevision) error {
+	if err := r.packageCommon.listPackageRevisions(ctx, filter, func(p repository.PackageRevision) error {
 		item, err := p.GetResources(ctx)
 		if err != nil {
 			return err
@@ -92,7 +93,7 @@ func (r *packageRevisionResources) Get(ctx context.Context, name string, options
 	ctx, span := tracer.Start(ctx, "packageRevisionResources::Get", trace.WithAttributes())
 	defer span.End()
 
-	pkg, err := r.packageCommon.getPackage(ctx, name)
+	pkg, err := r.packageCommon.getPackageRevision(ctx, name)
 	if err != nil {
 		return nil, err
 	}
@@ -116,7 +117,7 @@ func (r *packageRevisionResources) Update(ctx context.Context, name string, objI
 		return nil, false, apierrors.NewBadRequest("namespace must be specified")
 	}
 
-	oldPackage, err := r.packageCommon.getPackage(ctx, name)
+	oldPackage, err := r.packageCommon.getPackageRevision(ctx, name)
 	if err != nil {
 		return nil, false, err
 	}
@@ -154,7 +155,7 @@ func (r *packageRevisionResources) Update(ctx context.Context, name string, objI
 	repositoryID := types.NamespacedName{Namespace: ns, Name: repositoryName}
 	if err := r.coreClient.Get(ctx, repositoryID, &repositoryObj); err != nil {
 		if apierrors.IsNotFound(err) {
-			return nil, false, apierrors.NewNotFound(api.PackageRevisionResourcesGVR.GroupResource(), repositoryID.Name)
+			return nil, false, apierrors.NewNotFound(schema.GroupResource(api.PackageRevisionResourcesGVR.GroupResource()), repositoryID.Name)
 		}
 		return nil, false, apierrors.NewInternalError(fmt.Errorf("error getting repository %v: %w", repositoryID, err))
 	}
diff --git a/porch/pkg/registry/porch/packagerevisions_approval.go b/porch/pkg/registry/porch/packagerevisions_approval.go
index a1b13f57..3e885bc6 100644
--- a/porch/pkg/registry/porch/packagerevisions_approval.go
+++ b/porch/pkg/registry/porch/packagerevisions_approval.go
@@ -47,7 +47,12 @@ func (a *packageRevisionsApproval) NamespaceScoped() bool {
 }
 
 func (a *packageRevisionsApproval) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
-	return a.common.getPackageRevision(ctx, name, options)
+	pkg, err := a.common.getPackageRevision(ctx, name)
+	if err != nil {
+		return nil, err
+	}
+	obj := pkg.GetPackageRevision()
+	return obj, nil
 }
 
 // Update finds a resource in the storage and updates it. Some implementations
diff --git a/porch/pkg/registry/porch/storage.go b/porch/pkg/registry/porch/storage.go
index 5618451c..e6df7207 100644
--- a/porch/pkg/registry/porch/storage.go
+++ b/porch/pkg/registry/porch/storage.go
@@ -28,6 +28,18 @@ import (
 )
 
 func NewRESTStorage(scheme *runtime.Scheme, codecs serializer.CodecFactory, cad engine.CaDEngine, coreClient client.WithWatch) (genericapiserver.APIGroupInfo, error) {
+	packages := &packages{
+		TableConvertor: packageTableConvertor,
+		packageCommon: packageCommon{
+			scheme:         scheme,
+			cad:            cad,
+			gr:             porch.Resource("packages"),
+			coreClient:     coreClient,
+			updateStrategy: packageStrategy{},
+			createStrategy: packageStrategy{},
+		},
+	}
+
 	packageRevisions := &packageRevisions{
 		TableConvertor: packageRevisionTableConvertor,
 		packageCommon: packageCommon{
@@ -71,6 +83,7 @@ func NewRESTStorage(scheme *runtime.Scheme, codecs serializer.CodecFactory, cad
 
 	group.VersionedResourcesStorageMap = map[string]map[string]rest.Storage{
 		apiv1alpha1.SchemeGroupVersion.Version: {
+			"packages":                  packages,
 			"packagerevisions":          packageRevisions,
 			"packagerevisions/approval": packageRevisionsApproval,
 			"packagerevisionresources":  packageRevisionResources,
@@ -78,6 +91,15 @@ func NewRESTStorage(scheme *runtime.Scheme, codecs serializer.CodecFactory, cad
 		},
 	}
 
+	{
+		gvk := schema.GroupVersionKind{
+			Group:   apiv1alpha1.GroupName,
+			Version: apiv1alpha1.SchemeGroupVersion.Version,
+			Kind:    "Package",
+		}
+
+		scheme.AddFieldLabelConversionFunc(gvk, convertPackageFieldSelector)
+	}
 	{
 		gvk := schema.GroupVersionKind{
 			Group:   apiv1alpha1.GroupName,
diff --git a/porch/pkg/registry/porch/strategy.go b/porch/pkg/registry/porch/strategy.go
new file mode 100644
index 00000000..84ca714d
--- /dev/null
+++ b/porch/pkg/registry/porch/strategy.go
@@ -0,0 +1,129 @@
+// 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 porch
+
+import (
+	"context"
+	"fmt"
+	"strings"
+
+	api "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/util/validation/field"
+)
+
+// PackageRevisions Update Strategy
+
+type packageRevisionStrategy struct{}
+
+var _ SimpleRESTUpdateStrategy = packageRevisionStrategy{}
+
+func (s packageRevisionStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
+}
+
+func (s packageRevisionStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
+	allErrs := field.ErrorList{}
+	oldRevision := old.(*api.PackageRevision)
+	newRevision := obj.(*api.PackageRevision)
+
+	switch lifecycle := oldRevision.Spec.Lifecycle; lifecycle {
+	case "", api.PackageRevisionLifecycleDraft, api.PackageRevisionLifecycleProposed:
+		// valid
+
+	default:
+		allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "lifecycle"), lifecycle, fmt.Sprintf("can only update package with lifecycle value one of %s",
+			strings.Join([]string{
+				string(api.PackageRevisionLifecycleDraft),
+				string(api.PackageRevisionLifecycleProposed),
+			}, ",")),
+		))
+
+	}
+
+	switch lifecycle := newRevision.Spec.Lifecycle; lifecycle {
+	case "", api.PackageRevisionLifecycleDraft, api.PackageRevisionLifecycleProposed:
+		// valid
+
+	default:
+		allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "lifecycle"), lifecycle, fmt.Sprintf("value can be only updated to %s",
+			strings.Join([]string{
+				string(api.PackageRevisionLifecycleDraft),
+				string(api.PackageRevisionLifecycleProposed),
+			}, ",")),
+		))
+	}
+
+	return allErrs
+}
+
+func (s packageRevisionStrategy) Canonicalize(obj runtime.Object) {
+	pr := obj.(*api.PackageRevision)
+	if pr.Spec.Lifecycle == "" {
+		// Set default
+		pr.Spec.Lifecycle = api.PackageRevisionLifecycleDraft
+	}
+}
+
+var _ SimpleRESTCreateStrategy = packageRevisionStrategy{}
+
+// Validate returns an ErrorList with validation errors or nil.  Validate
+// is invoked after default fields in the object have been filled in
+// before the object is persisted.  This method should not mutate the
+// object.
+func (s packageRevisionStrategy) Validate(ctx context.Context, runtimeObj runtime.Object) field.ErrorList {
+	allErrs := field.ErrorList{}
+
+	obj := runtimeObj.(*api.PackageRevision)
+
+	switch lifecycle := obj.Spec.Lifecycle; lifecycle {
+	case "", api.PackageRevisionLifecycleDraft:
+		// valid
+
+	default:
+		allErrs = append(allErrs, field.Invalid(field.NewPath("spec", "lifecycle"), lifecycle, fmt.Sprintf("value can be only created as %s",
+			strings.Join([]string{
+				string(api.PackageRevisionLifecycleDraft),
+			}, ",")),
+		))
+	}
+
+	return allErrs
+}
+
+// Package Update Strategy
+
+type packageStrategy struct{}
+
+var _ SimpleRESTUpdateStrategy = packageStrategy{}
+
+func (s packageStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
+}
+
+func (s packageStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
+	return nil
+}
+
+func (s packageStrategy) Canonicalize(obj runtime.Object) {
+}
+
+var _ SimpleRESTCreateStrategy = packageStrategy{}
+
+// Validate returns an ErrorList with validation errors or nil.  Validate
+// is invoked after default fields in the object have been filled in
+// before the object is persisted.  This method should not mutate the
+// object.
+func (s packageStrategy) Validate(ctx context.Context, runtimeObj runtime.Object) field.ErrorList {
+	return nil
+}
diff --git a/porch/pkg/registry/porch/table.go b/porch/pkg/registry/porch/table.go
index 31e4d146..a593c13a 100644
--- a/porch/pkg/registry/porch/table.go
+++ b/porch/pkg/registry/porch/table.go
@@ -84,6 +84,28 @@ func (c tableConvertor) ConvertToTable(ctx context.Context, object runtime.Objec
 }
 
 var (
+	packageTableConvertor = tableConvertor{
+		resource: porch.Resource("packages"),
+		cells: func(obj runtime.Object) []interface{} {
+			pr, ok := obj.(*api.Package)
+			if !ok {
+				return nil
+			}
+			return []interface{}{
+				pr.Name,
+				pr.Spec.PackageName,
+				pr.Spec.RepositoryName,
+				pr.Status.LatestRevision,
+			}
+		},
+		columns: []metav1.TableColumnDefinition{
+			{Name: "Name", Type: "string"},
+			{Name: "Package", Type: "string"},
+			{Name: "Repository", Type: "string"},
+			{Name: "Latest Revision", Type: "string"},
+		},
+	}
+
 	packageRevisionTableConvertor = tableConvertor{
 		resource: porch.Resource("packagerevisions"),
 		cells: func(obj runtime.Object) []interface{} {
diff --git a/porch/pkg/repository/repository.go b/porch/pkg/repository/repository.go
index 96e30ac3..77902b50 100644
--- a/porch/pkg/repository/repository.go
+++ b/porch/pkg/repository/repository.go
@@ -36,6 +36,14 @@ func (n PackageRevisionKey) String() string {
 	return fmt.Sprintf("Repository: %q, Package: %q, Revision: %q", n.Repository, n.Package, n.Revision)
 }
 
+type PackageKey struct {
+	Repository, Package string
+}
+
+func (n PackageKey) String() string {
+	return fmt.Sprintf("Repository: %q, Package: %q", n.Repository, n.Package)
+}
+
 // PackageRevision is an abstract package version.
 // We have a single object for both Revision and Resources, because conceptually they are one object.
 // The best way we've found (so far) to represent them in k8s is as two resources, but they map to the same object.
@@ -66,6 +74,23 @@ type PackageRevision interface {
 	GetLock() (kptfile.Upstream, kptfile.UpstreamLock, error)
 }
 
+// Package is an abstract package.
+type Package interface {
+	// KubeObjectName returns an encoded name for the object that should be unique.
+	// More "readable" values are returned by Key()
+	KubeObjectName() string
+
+	// Key returns the "primary key" of the package.
+	Key() PackageKey
+
+	// GetPackage returns the object representing this package
+	GetPackage() *v1alpha1.Package
+
+	// GetLatestRevision returns the name of the package revision that is the "latest" package
+	// revision belonging to this package
+	GetLatestRevision() string
+}
+
 type PackageDraft interface {
 	UpdateResources(ctx context.Context, new *v1alpha1.PackageRevisionResources, task *v1alpha1.Task) error
 	// Updates desired lifecycle of the package. The lifecycle is applied on Close.
@@ -93,7 +118,7 @@ type ListPackageRevisionFilter struct {
 	Revision string
 }
 
-// Matches returns true if the provided PackageRevision satisifies the conditions in the filter.
+// Matches returns true if the provided PackageRevision satisfies the conditions in the filter.
 func (f *ListPackageRevisionFilter) Matches(p PackageRevision) bool {
 	if f.Package != "" && f.Package != p.Key().Package {
 		return false
@@ -107,9 +132,31 @@ func (f *ListPackageRevisionFilter) Matches(p PackageRevision) bool {
 	return true
 }
 
+// ListPackageFilter is a predicate for filtering Package objects;
+// only matching Package objects will be returned.
+type ListPackageFilter struct {
+	// KubeObjectName matches the generated kubernetes object name.
+	KubeObjectName string
+
+	// Package matches the name of the package (spec.package)
+	Package string
+}
+
+// Matches returns true if the provided Package satisfies the conditions in the filter.
+func (f *ListPackageFilter) Matches(p Package) bool {
+	if f.Package != "" && f.Package != p.Key().Package {
+		return false
+	}
+	if f.KubeObjectName != "" && f.KubeObjectName != p.KubeObjectName() {
+		return false
+	}
+	return true
+}
+
 // Repository is the interface for interacting with packages in repositories
 // TODO: we may need interface to manage repositories too. Stay tuned.
 type Repository interface {
+	// ListPackageRevisions lists the existing package revisions in the repository
 	ListPackageRevisions(ctx context.Context, filter ListPackageRevisionFilter) ([]PackageRevision, error)
 
 	// CreatePackageRevision creates a new package revision
@@ -118,8 +165,17 @@ type Repository interface {
 	// DeletePackageRevision deletes a package revision
 	DeletePackageRevision(ctx context.Context, old PackageRevision) error
 
-	// UpdatePackage updates a package
-	UpdatePackage(ctx context.Context, old PackageRevision) (PackageDraft, error)
+	// UpdatePackageRevision updates a package
+	UpdatePackageRevision(ctx context.Context, old PackageRevision) (PackageDraft, error)
+
+	// ListPackages lists all packages in the repository
+	ListPackages(ctx context.Context, filter ListPackageFilter) ([]Package, error)
+
+	// CreatePackage creates a new package
+	CreatePackage(ctx context.Context, obj *v1alpha1.Package) (Package, error)
+
+	// DeletePackage deletes a package
+	DeletePackage(ctx context.Context, old Package) error
 }
 
 type FunctionRepository interface {
diff --git a/porch/scripts/create-deployment-blueprint.sh b/porch/scripts/create-deployment-blueprint.sh
index 29a594ce..08da1c39 100755
--- a/porch/scripts/create-deployment-blueprint.sh
+++ b/porch/scripts/create-deployment-blueprint.sh
@@ -46,6 +46,7 @@ FUNCTION_IMAGE=""
 WRAPPER_SERVER_IMAGE=""
 SERVER_SA=""
 CONTROLLERS_SA=""
+FUNCTION_RUNNER_SA=""
 PROJECT=""
 
 while [[ $# -gt 0 ]]; do
@@ -100,6 +101,7 @@ done
 # Defaults
 
 if [ -n "${PROJECT}" ]; then
+  FUNCTION_RUNNER_SA="${FUNCTION_RUNNER_SA:-iam.gke.io/gcp-service-account=porch-function-runner@${PROJECT}.iam.gserviceaccount.com}"
   CONTROLLERS_SA="${CONTROLLERS_SA:-iam.gke.io/gcp-service-account=porch-sync@${PROJECT}.iam.gserviceaccount.com}"
   SERVER_SA="${SERVER_SA:-iam.gke.io/gcp-service-account=porch-server@${PROJECT}.iam.gserviceaccount.com}"
 fi
@@ -204,6 +206,11 @@ function main() {
   if [ -n "${SERVER_SA}" ]; then
     customize-sa "porch-server" "${SERVER_SA}"
   fi
+
+  if [ -n "${FUNCTION_RUNNER_SA}" ]; then
+    # TODO: Rename serviceaccount for consistency (avoid abbreviations?)
+    customize-sa "porch-fn-runner" "${FUNCTION_RUNNER_SA}"
+  fi
 }
 
 validate
diff --git a/thirdparty/cmdconfig/commands/cmdeval/cmdeval.go b/thirdparty/cmdconfig/commands/cmdeval/cmdeval.go
index 27e6be84..b89d1b6b 100644
--- a/thirdparty/cmdconfig/commands/cmdeval/cmdeval.go
+++ b/thirdparty/cmdconfig/commands/cmdeval/cmdeval.go
@@ -36,6 +36,8 @@ import (
 // GetEvalFnRunner returns a EvalFnRunner.
 func GetEvalFnRunner(ctx context.Context, parent string) *EvalFnRunner {
 	r := &EvalFnRunner{Ctx: ctx}
+	r.InitDefaults()
+
 	c := &cobra.Command{
 		Use:     "eval [DIR | -] [flags] [--fn-args]",
 		Short:   docs.EvalShort,
@@ -83,8 +85,13 @@ func GetEvalFnRunner(ctx context.Context, parent string) *EvalFnRunner {
 		"a list of environment variables to be used by functions")
 	r.Command.Flags().BoolVar(
 		&r.AsCurrentUser, "as-current-user", false, "use the uid and gid that kpt is running with to run the function in the container")
-	r.Command.Flags().StringVar(&r.ImagePullPolicy, "image-pull-policy", string(fnruntime.IfNotPresentPull),
-		fmt.Sprintf("pull image before running the container. It must be one of %s, %s and %s.", fnruntime.AlwaysPull, fnruntime.IfNotPresentPull, fnruntime.NeverPull))
+
+	r.Command.Flags().Var(&r.ImagePullPolicy, "image-pull-policy",
+		"pull image before running the container "+r.ImagePullPolicy.HelpAllowedValues())
+	r.Command.RegisterFlagCompletionFunc("image-pull-policy", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+		return r.ImagePullPolicy.AllStrings(), cobra.ShellCompDirectiveDefault
+	})
+
 	r.Command.Flags().BoolVar(
 		&r.AllowWasm, "allow-alpha-wasm", false, "allow alpha wasm functions to be run. If true, you can specify a wasm image with --image flag or a path to a wasm file (must have the .wasm file extension) with --exec flag.")
 
@@ -141,7 +148,7 @@ type EvalFnRunner struct {
 	FnConfigPath         string
 	RunFns               runfn.RunFns
 	ResultsDir           string
-	ImagePullPolicy      string
+	ImagePullPolicy      fnruntime.ImagePullPolicy
 	Network              bool
 	Mounts               []string
 	Env                  []string
@@ -160,6 +167,10 @@ type EvalFnRunner struct {
 	excludeAnnotations  []string
 }
 
+func (r *EvalFnRunner) InitDefaults() {
+	r.ImagePullPolicy = fnruntime.IfNotPresentPull
+}
+
 func (r *EvalFnRunner) runE(c *cobra.Command, _ []string) error {
 	err := runner.HandleError(r.Ctx, r.RunFns.Execute())
 	if err != nil {
@@ -430,9 +441,6 @@ func (r *EvalFnRunner) validateOptionalFlags() error {
 		}
 	}
 
-	if err := cmdutil.ValidateImagePullPolicyValue(r.ImagePullPolicy); err != nil {
-		return err
-	}
 	return nil
 }
 
@@ -539,7 +547,7 @@ func (r *EvalFnRunner) preRunE(c *cobra.Command, args []string) error {
 		AsCurrentUser:   r.AsCurrentUser,
 		FnConfig:        fnConfig,
 		FnConfigPath:    r.FnConfigPath,
-		ImagePullPolicy: cmdutil.StringToImagePullPolicy(r.ImagePullPolicy),
+		ImagePullPolicy: r.ImagePullPolicy,
 		// fn eval should remove all files when all resources
 		// are deleted.
 		ContinueOnEmptyResult: true,



Test patch:
diff --git a/porch/pkg/cache/cache_test.go b/porch/pkg/cache/cache_test.go
index 7026ea8e..d1b434c7 100644
--- a/porch/pkg/cache/cache_test.go
+++ b/porch/pkg/cache/cache_test.go
@@ -94,7 +94,7 @@ func TestPublishedLatest(t *testing.T) {
 		t.Fatalf("Bucket package lifecycle: got %s, want %s", got, want)
 	}
 
-	update, err := cached.UpdatePackage(ctx, bucket)
+	update, err := cached.UpdatePackageRevision(ctx, bucket)
 	if err != nil {
 		t.Fatalf("UpdatePackaeg(%s) failed: %v", bucket.Key(), err)
 	}
diff --git a/porch/pkg/engine/edit_test.go b/porch/pkg/engine/edit_test.go
index ec5b4e83..c9f6dfc9 100644
--- a/porch/pkg/engine/edit_test.go
+++ b/porch/pkg/engine/edit_test.go
@@ -107,6 +107,8 @@ type fakeCaD struct {
 	repository repository.Repository
 }
 
+var _ CaDEngine = &fakeCaD{}
+
 func (f *fakeCaD) OpenRepository(context.Context, *configapi.Repository) (repository.Repository, error) {
 	return f.repository, nil
 }
@@ -130,3 +132,15 @@ func (f *fakeCaD) DeletePackageRevision(context.Context, *configapi.Repository,
 func (f *fakeCaD) ListFunctions(context.Context, *configapi.Repository) ([]repository.Function, error) {
 	return []repository.Function{}, nil
 }
+
+func (f *fakeCaD) CreatePackage(context.Context, *configapi.Repository, *v1alpha1.Package) (repository.Package, error) {
+	return nil, nil
+}
+
+func (f *fakeCaD) UpdatePackage(_ context.Context, _ *configapi.Repository, _ repository.Package, _, _ *v1alpha1.Package) (repository.Package, error) {
+	return nil, nil
+}
+
+func (f *fakeCaD) DeletePackage(context.Context, *configapi.Repository, repository.Package) error {
+	return nil
+}
diff --git a/porch/pkg/git/git_test.go b/porch/pkg/git/git_test.go
index d3b3240c..28e99896 100644
--- a/porch/pkg/git/git_test.go
+++ b/porch/pkg/git/git_test.go
@@ -192,15 +192,15 @@ func (g GitSuite) TestGitPackageRoundTrip(t *testing.T) {
 			t.Fatalf("ListPackageRevisons failed: %v", err)
 		}
 
-		original := findPackage(t, revisions, repository.PackageRevisionKey{
+		original := findPackageRevision(t, revisions, repository.PackageRevisionKey{
 			Repository: repositoryName,
 			Package:    packageName,
 			Revision:   revision,
 		})
 
-		update, err := repo.UpdatePackage(ctx, original)
+		update, err := repo.UpdatePackageRevision(ctx, original)
 		if err != nil {
-			t.Fatalf("UpdatePackage(%#v failed: %v", original, err)
+			t.Fatalf("UpdatePackageRevision(%#v failed: %v", original, err)
 		}
 		if err := update.UpdateLifecycle(ctx, v1alpha1.PackageRevisionLifecyclePublished); err != nil {
 			t.Fatalf("UpdateLifecycle failed: %v", err)
@@ -218,9 +218,9 @@ func (g GitSuite) TestGitPackageRoundTrip(t *testing.T) {
 		version := "v123"
 
 		path := "test-package"
-		packageRevision, gitLock, err := repo.GetPackage(ctx, version, path)
+		packageRevision, gitLock, err := repo.GetPackageRevision(ctx, version, path)
 		if err != nil {
-			t.Fatalf("GetPackage(%q, %q) failed: %v", version, path, err)
+			t.Fatalf("GetPackageRevision(%q, %q) failed: %v", version, path, err)
 		}
 
 		t.Logf("packageRevision is %s", packageRevision.KubeObjectName())
@@ -646,7 +646,7 @@ func (g GitSuite) TestApproveDraft(t *testing.T) {
 		t.Fatalf("ListPackageRevisions failed: %v", err)
 	}
 
-	bucket := findPackage(t, revisions, repository.PackageRevisionKey{
+	bucket := findPackageRevision(t, revisions, repository.PackageRevisionKey{
 		Repository: repositoryName,
 		Package:    "bucket",
 		Revision:   "v1",
@@ -656,9 +656,9 @@ func (g GitSuite) TestApproveDraft(t *testing.T) {
 	refMustExist(t, repo, draft.RefInRemote())
 	refMustNotExist(t, repo, finalReferenceName)
 
-	update, err := git.UpdatePackage(ctx, bucket)
+	update, err := git.UpdatePackageRevision(ctx, bucket)
 	if err != nil {
-		t.Fatalf("UpdatePackage failed: %v", err)
+		t.Fatalf("UpdatePackageRevision failed: %v", err)
 	}
 
 	update.UpdateLifecycle(ctx, v1alpha1.PackageRevisionLifecyclePublished)
@@ -704,7 +704,7 @@ func (g GitSuite) TestApproveDraftWithHistory(t *testing.T) {
 		t.Fatalf("ListPackageRevisions failed: %v", err)
 	}
 
-	bucket := findPackage(t, revisions, repository.PackageRevisionKey{
+	bucket := findPackageRevision(t, revisions, repository.PackageRevisionKey{
 		Repository: repositoryName,
 		Package:    "pkg-with-history",
 		Revision:   "v1",
@@ -714,9 +714,9 @@ func (g GitSuite) TestApproveDraftWithHistory(t *testing.T) {
 	refMustExist(t, repo, draft.RefInRemote())
 	refMustNotExist(t, repo, finalReferenceName)
 
-	update, err := git.UpdatePackage(ctx, bucket)
+	update, err := git.UpdatePackageRevision(ctx, bucket)
 	if err != nil {
-		t.Fatalf("UpdatePackage failed: %v", err)
+		t.Fatalf("UpdatePackageRevision failed: %v", err)
 	}
 
 	update.UpdateLifecycle(ctx, v1alpha1.PackageRevisionLifecyclePublished)
@@ -870,7 +870,7 @@ func (g GitSuite) TestRefreshRepo(t *testing.T) {
 	}
 
 	// Confirm we listed some package(s)
-	findPackage(t, all, repository.PackageRevisionKey{Repository: "refresh", Package: "basens", Revision: "v2"})
+	findPackageRevision(t, all, repository.PackageRevisionKey{Repository: "refresh", Package: "basens", Revision: "v2"})
 	packageMustNotExist(t, all, newPackageName)
 
 	// Create package in the upstream repository
@@ -921,7 +921,7 @@ func (g GitSuite) TestRefreshRepo(t *testing.T) {
 	if err != nil {
 		t.Fatalf("ListPackageRevisions(Refresh) failed; %v", err)
 	}
-	findPackage(t, all, newPackageName)
+	findPackageRevision(t, all, newPackageName)
 }
 
 // The test deletes packages on the upstream one by one and validates they were
@@ -1190,7 +1190,7 @@ func (g GitSuite) TestAuthor(t *testing.T) {
 			}
 
 			_ = revisions
-			draftPkg := findPackage(t, revisions, repository.PackageRevisionKey{
+			draftPkg := findPackageRevision(t, revisions, repository.PackageRevisionKey{
 				Repository: repositoryName,
 				Package:    tc.pkg,
 				Revision:   tc.revision,
diff --git a/porch/pkg/git/testing_repo.go b/porch/pkg/git/testing_repo.go
index 539ffb1a..bf2338ff 100644
--- a/porch/pkg/git/testing_repo.go
+++ b/porch/pkg/git/testing_repo.go
@@ -253,7 +253,7 @@ func findFile(t *testing.T, tree *object.Tree, path string) *object.File {
 	return file
 }
 
-func findPackage(t *testing.T, revisions []repository.PackageRevision, key repository.PackageRevisionKey) repository.PackageRevision {
+func findPackageRevision(t *testing.T, revisions []repository.PackageRevision, key repository.PackageRevisionKey) repository.PackageRevision {
 	t.Helper()
 
 	for _, r := range revisions {
@@ -282,7 +282,7 @@ func repositoryMustHavePackageRevision(t *testing.T, git GitRepository, name rep
 	if err != nil {
 		t.Fatalf("ListPackageRevisions failed: %v", err)
 	}
-	findPackage(t, list, name)
+	findPackageRevision(t, list, name)
 }
 
 func repositoryMustNotHavePackageRevision(t *testing.T, git GitRepository, name repository.PackageRevisionKey) {