Repository: GoogleContainerTools/kpt
Pull request #3068 (2022-04-29 01:28:11)

Update kpt alpha command help
While authoring the user guide, found several opportunities to
improve command help text.




Patch:
diff --git a/internal/cmdrpkgapprove/command.go b/internal/cmdrpkgapprove/command.go
index 6e6ea0e8..41725659 100644
--- a/internal/cmdrpkgapprove/command.go
+++ b/internal/cmdrpkgapprove/command.go
@@ -57,7 +57,7 @@ func newRunner(ctx context.Context, rcg *genericclioptions.ConfigFlags) *runner
 		Use:     "approve PACKAGE",
 		Short:   "Approves a proposal to finalize a package revision.",
 		Long:    longMsg,
-		Example: "kpt alpha rpkg approve git-repository:package-revision:v3",
+		Example: "kpt alpha rpkg approve package-name",
 		PreRunE: r.preRunE,
 		RunE:    r.runE,
 		Hidden:  porch.HidePorchCommands,
diff --git a/internal/cmdrpkgclone/command.go b/internal/cmdrpkgclone/command.go
index c00f3d05..60f48aba 100644
--- a/internal/cmdrpkgclone/command.go
+++ b/internal/cmdrpkgclone/command.go
@@ -41,7 +41,7 @@ SOURCE_PACKAGE:
   Source package. Can be a reference to an OCI package, Git package, or an package resource name:
     * oci://oci-repository/package-name
     * http://git-repository.git/package-name
-    * repository:package:revision
+    * package-revision-name
 
 NAME:
   Target package revision name (downstream package)
diff --git a/internal/cmdrpkginit/command.go b/internal/cmdrpkginit/command.go
index 22744c12..aa1b2119 100644
--- a/internal/cmdrpkginit/command.go
+++ b/internal/cmdrpkginit/command.go
@@ -43,19 +43,19 @@ PACKAGE:
 Flags:
 
 --repository
-  Repository to which package will be cloned (downstream repository).
+  Repository in which the package will be created.
 
 --revision
-  Revision of the downstream package.
+  Revision of the new package.
 
 --description
-  short description of the package
+  Short description of the package
 
 --keywords
-  list of keywords for the package
+  List of keywords for the package
 
 --site
-  link to page with information about the package
+  Link to page with information about the package
 
 `
 )
diff --git a/internal/cmdrpkgpull/command.go b/internal/cmdrpkgpull/command.go
index 4734b345..33fb5829 100644
--- a/internal/cmdrpkgpull/command.go
+++ b/internal/cmdrpkgpull/command.go
@@ -69,7 +69,7 @@ func newRunner(ctx context.Context, rcg *genericclioptions.ConfigFlags) *runner
 		SuggestFor: []string{},
 		Short:      "Reads package resources.",
 		Long:       longMsg,
-		Example:    "kpt alpha rpkg pull repository:package:v1 ./package-directory",
+		Example:    "kpt alpha rpkg pull package-name ./package-directory",
 		PreRunE:    r.preRunE,
 		RunE:       r.runE,
 		Hidden:     porch.HidePorchCommands,
diff --git a/internal/cmdrpkgreject/command.go b/internal/cmdrpkgreject/command.go
index 9bd4d536..f4f8d9b7 100644
--- a/internal/cmdrpkgreject/command.go
+++ b/internal/cmdrpkgreject/command.go
@@ -57,7 +57,7 @@ func newRunner(ctx context.Context, rcg *genericclioptions.ConfigFlags) *runner
 		Use:     "reject PACKAGE",
 		Short:   "Rejects a proposal to finalize a package revision",
 		Long:    longMsg,
-		Example: "kpt alpha rpkg reject git-repository:package-revision:v3",
+		Example: "kpt alpha rpkg reject package-name",
 		PreRunE: r.preRunE,
 		RunE:    r.runE,
 		Hidden:  porch.HidePorchCommands,
diff --git a/internal/cmdsync/common.go b/internal/cmdsync/common.go
new file mode 100644
index 00000000..87ea9c91
--- /dev/null
+++ b/internal/cmdsync/common.go
@@ -0,0 +1,19 @@
+// 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 cmdsync
+
+const (
+	RootSyncNamespace = "config-management-system"
+)
diff --git a/internal/cmdsync/create/command.go b/internal/cmdsync/create/command.go
index 76e20a09..bbe9e5cf 100644
--- a/internal/cmdsync/create/command.go
+++ b/internal/cmdsync/create/command.go
@@ -18,6 +18,7 @@ import (
 	"context"
 	"fmt"
 
+	"github.com/GoogleContainerTools/kpt/internal/cmdsync"
 	"github.com/GoogleContainerTools/kpt/internal/errors"
 	"github.com/GoogleContainerTools/kpt/internal/util/porch"
 	porchapi "github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1"
@@ -45,7 +46,6 @@ Flags:
 --package
   Package revision to sync into the deployment cluster. Required flag.
 `
-	rootSyncNamespace = "config-management-system"
 )
 
 func newRunner(ctx context.Context, rcg *genericclioptions.ConfigFlags) *runner {
@@ -147,7 +147,7 @@ func (r *runner) runE(cmd *cobra.Command, args []string) error {
 			},
 			ObjectMeta: metav1.ObjectMeta{
 				Name:      fmt.Sprintf("%s-auth", syncName),
-				Namespace: rootSyncNamespace,
+				Namespace: cmdsync.RootSyncNamespace,
 			},
 			Data: map[string][]byte{
 				"username": repositorySecret.Data["username"],
@@ -180,7 +180,7 @@ func (r *runner) runE(cmd *cobra.Command, args []string) error {
 			"kind":       "RootSync",
 			"metadata": map[string]interface{}{
 				"name":      syncName,
-				"namespace": rootSyncNamespace,
+				"namespace": cmdsync.RootSyncNamespace,
 			},
 			"spec": map[string]interface{}{
 				"sourceFormat": "unstructured",
diff --git a/internal/cmdsync/delete/command.go b/internal/cmdsync/delete/command.go
index 3de2e2db..a11f8581 100644
--- a/internal/cmdsync/delete/command.go
+++ b/internal/cmdsync/delete/command.go
@@ -19,6 +19,7 @@ import (
 	"fmt"
 	"time"
 
+	"github.com/GoogleContainerTools/kpt/internal/cmdsync"
 	"github.com/GoogleContainerTools/kpt/internal/errors"
 	"github.com/GoogleContainerTools/kpt/internal/util/porch"
 	"github.com/spf13/cobra"
@@ -123,7 +124,10 @@ func (r *runner) runE(cmd *cobra.Command, args []string) error {
 	}
 
 	name := args[0]
-	namespace := *r.cfg.Namespace
+	namespace := cmdsync.RootSyncNamespace
+	if *r.cfg.Namespace != "" {
+		namespace = *r.cfg.Namespace
+	}
 	key := client.ObjectKey{
 		Namespace: namespace,
 		Name:      name,
@@ -152,7 +156,7 @@ func (r *runner) runE(cmd *cobra.Command, args []string) error {
 		return errors.E(op, err)
 	}
 
-	fmt.Println("Deleting package resources..")
+	fmt.Println("Deleting synced resources..")
 	if err := r.client.Update(r.ctx, &rs); err != nil {
 		return errors.E(op, err)
 	}
@@ -203,7 +207,7 @@ func (r *runner) runE(cmd *cobra.Command, args []string) error {
 		return errors.E(op, fmt.Errorf("failed to delete Secret %s: %w", secret, err))
 	}
 
-	fmt.Printf("Package %s successfully deleted\n", name)
+	fmt.Printf("Sync %s successfully deleted\n", name)
 	return nil
 }
 
diff --git a/internal/cmdsync/get/command.go b/internal/cmdsync/get/command.go
index a43854e4..8f68a0ad 100644
--- a/internal/cmdsync/get/command.go
+++ b/internal/cmdsync/get/command.go
@@ -18,6 +18,7 @@ import (
 	"context"
 	"fmt"
 
+	"github.com/GoogleContainerTools/kpt/internal/cmdsync"
 	"github.com/GoogleContainerTools/kpt/internal/errors"
 	"github.com/GoogleContainerTools/kpt/internal/util/porch"
 	"github.com/spf13/cobra"
@@ -51,7 +52,7 @@ func newRunner(ctx context.Context, rcg *genericclioptions.ConfigFlags) *runner
 		Aliases: []string{"register"},
 		Short:   "Gets a RootSync resource with which package was deployed.",
 		Long:    longMsg,
-		Example: "kpt alpha sync get sync-resource --namespace=default",
+		Example: "kpt alpha sync get sync-resource",
 		PreRunE: r.preRunE,
 		RunE:    r.runE,
 		Hidden:  porch.HidePorchCommands,
@@ -96,8 +97,12 @@ func (r *runner) runE(cmd *cobra.Command, args []string) error {
 	}
 
 	name := args[0]
+	namespace := cmdsync.RootSyncNamespace
+	if *r.cfg.Namespace != "" {
+		namespace = *r.cfg.Namespace
+	}
 	key := client.ObjectKey{
-		Namespace: *r.cfg.Namespace,
+		Namespace: namespace,
 		Name:      name,
 	}
 	rs := unstructured.Unstructured{
diff --git a/porch/config/deploy/2-function-runner.yaml b/porch/config/deploy/2-function-runner.yaml
index 10612df8..35785192 100644
--- a/porch/config/deploy/2-function-runner.yaml
+++ b/porch/config/deploy/2-function-runner.yaml
@@ -40,9 +40,13 @@ spec:
           image: gcr.io/example-google-project-id/porch-function-runner:latest
           imagePullPolicy: IfNotPresent
           command:
-            - sh
-            - -c
-            - /server --config=/config.yaml --functions=/functions --pod-namespace=porch-fn-system --wrapper-server-image=gcr.io/example-google-project-id/porch-wrapper-server:latest
+            - /server
+            - --config=/config.yaml
+            - --functions=/functions
+            - --pod-namespace=porch-fn-system
+          env:
+            - name: WRAPPER_SERVER_IMAGE
+              value: gcr.io/example-google-project-id/porch-wrapper-server:latest
           ports:
             - containerPort: 9445
           # Add grpc readiness probe to ensure the cache is ready
diff --git a/porch/config/development/tracing/README.md b/porch/config/development/tracing/README.md
new file mode 100644
index 00000000..9e7065ec
--- /dev/null
+++ b/porch/config/development/tracing/README.md
@@ -0,0 +1,30 @@
+### How to enable jaeger tracing
+
+If you want to enable jaeger tracing of the porch-server:
+
+* Apply the [deployment.yaml manifest](deployment.yaml) from this directory
+
+```
+kubectl apply -f deployment.yaml
+```
+
+* Add the commented out env var OTEL to the porch-server manifest:
+
+```
+kubectl edit deployment -n porch-system porch-server
+```
+
+```
+        env:
+          # Uncomment to enable trace-reporting to jaeger
+          #- name: OTEL
+          #  value: otel://jaeger-oltp:4317
+```
+
+* Port-forward the jaeger http port to your local machine:
+
+```
+kubectl port-forward -n porch-system service/jaeger-http 16686
+```
+
+* Open your browser to the UI on http://localhost:16686
\ No newline at end of file
diff --git a/porch/func/Dockerfile-wrapperserver b/porch/func/Dockerfile-wrapperserver
index 0c9d212a..edd8b5b7 100644
--- a/porch/func/Dockerfile-wrapperserver
+++ b/porch/func/Dockerfile-wrapperserver
@@ -2,6 +2,9 @@ FROM golang:1.17.8-alpine3.15 as builder
 
 WORKDIR /go/src/github.com/GoogleContainerTools/kpt
 
+# Ensure the wrapper server and grpc-health-probe is statically linked so that they works in distroless-based images.
+ENV CGO_ENABLED=0
+
 COPY go.mod go.sum ./
 COPY porch/go.mod porch/go.sum porch/
 COPY porch/api/go.mod porch/api/go.sum porch/api/
diff --git a/porch/func/internal/podevaluator.go b/porch/func/internal/podevaluator.go
index c010fa0f..08ae5238 100644
--- a/porch/func/internal/podevaluator.go
+++ b/porch/func/internal/podevaluator.go
@@ -266,11 +266,12 @@ func (pcm *podCacheManager) podCacheManager() {
 			channels := pcm.waitlists[resp.image]
 			delete(pcm.waitlists, resp.image)
 			for i := range channels {
-				// The channel has one buffer size, nothing will be blocking.
-				channels[i] <- &clientConnAndError{
-					grpcClient: resp.grpcClient,
-					err:        resp.err,
+				cce := &clientConnAndError{err: resp.err}
+				if resp.podAndGRPCClient != nil {
+					cce.grpcClient = resp.podAndGRPCClient.grpcClient
 				}
+				// The channel has one buffer size, nothing will be blocking.
+				channels[i] <- cce
 			}
 		case <-tick:
 			// synchronous GC
@@ -497,8 +498,10 @@ func (pm *podManager) retrieveOrCreatePod(ctx context.Context, image string, ttl
 					Name:  "copy-wrapper-server",
 					Image: pm.wrapperServerImage,
 					Command: []string{
-						"sh", "-c",
-						fmt.Sprintf("cp /wrapper-server/* %v", volumeMountPath),
+						"cp",
+						"-a",
+						"/wrapper-server/.",
+						volumeMountPath,
 					},
 					VolumeMounts: []corev1.VolumeMount{
 						{
@@ -510,12 +513,9 @@ func (pm *podManager) retrieveOrCreatePod(ctx context.Context, image string, ttl
 			},
 			Containers: []corev1.Container{
 				{
-					Name:  "function",
-					Image: image,
-					Command: []string{
-						"sh", "-c",
-						strings.Join(cmd, " "),
-					},
+					Name:    "function",
+					Image:   image,
+					Command: cmd,
 					ReadinessProbe: &corev1.Probe{
 						ProbeHandler: corev1.ProbeHandler{
 							Exec: &corev1.ExecAction{
diff --git a/porch/func/server/server.go b/porch/func/server/server.go
index ccc19047..1b033721 100644
--- a/porch/func/server/server.go
+++ b/porch/func/server/server.go
@@ -33,18 +33,19 @@ import (
 const (
 	execRuntime = "exec"
 	podRuntime  = "pod"
+
+	wrapperServerImageEnv = "WRAPPER_SERVER_IMAGE"
 )
 
 var (
-	port               = flag.Int("port", 9445, "The server port")
-	functions          = flag.String("functions", "./functions", "Path to cached functions.")
-	config             = flag.String("config", "./config.yaml", "Path to the config file.")
-	podCacheConfig     = flag.String("pod-cache-config", "/pod-cache-config/pod-cache-config.yaml", "Path to the pod cache config file. The file is map of function name to TTL.")
-	podNamespace       = flag.String("pod-namespace", "porch-fn-system", "Namespace to run KRM functions pods.")
-	podTTL             = flag.Duration("pod-ttl", 30*time.Minute, "TTL for pods before GC.")
-	scanInterval       = flag.Duration("scan-interval", time.Minute, "The interval of GC between scans.")
-	wrapperServerImage = flag.String("wrapper-server-image", "", "Image name of the wrapper server.")
-	disableRuntimes    = flag.String("disable-runtimes", "", fmt.Sprintf("The runtime(s) to disable. Multiple runtimes should separated by `,`. Available runtimes: `%v`, `%v`.", execRuntime, podRuntime))
+	port            = flag.Int("port", 9445, "The server port")
+	functions       = flag.String("functions", "./functions", "Path to cached functions.")
+	config          = flag.String("config", "./config.yaml", "Path to the config file.")
+	podCacheConfig  = flag.String("pod-cache-config", "/pod-cache-config/pod-cache-config.yaml", "Path to the pod cache config file. The file is map of function name to TTL.")
+	podNamespace    = flag.String("pod-namespace", "porch-fn-system", "Namespace to run KRM functions pods.")
+	podTTL          = flag.Duration("pod-ttl", 30*time.Minute, "TTL for pods before GC.")
+	scanInterval    = flag.Duration("scan-interval", time.Minute, "The interval of GC between scans.")
+	disableRuntimes = flag.String("disable-runtimes", "", fmt.Sprintf("The runtime(s) to disable. Multiple runtimes should separated by `,`. Available runtimes: `%v`, `%v`.", execRuntime, podRuntime))
 )
 
 func main() {
@@ -83,7 +84,11 @@ func run() error {
 			}
 			runtimes = append(runtimes, execEval)
 		case podRuntime:
-			podEval, err := internal.NewPodEvaluator(*podNamespace, *wrapperServerImage, *scanInterval, *podTTL, *podCacheConfig)
+			wrapperServerImage := os.Getenv(wrapperServerImageEnv)
+			if wrapperServerImage == "" {
+				return fmt.Errorf("environment variable %v must be set to use pod function evaluator runtime", wrapperServerImageEnv)
+			}
+			podEval, err := internal.NewPodEvaluator(*podNamespace, wrapperServerImage, *scanInterval, *podTTL, *podCacheConfig)
 			if err != nil {
 				return fmt.Errorf("failed to initialize pod evaluator: %w", err)
 			}
diff --git a/porch/hack/create-deployment-blueprint.sh b/porch/hack/create-deployment-blueprint.sh
index 0dff8d7d..24c3c34a 100755
--- a/porch/hack/create-deployment-blueprint.sh
+++ b/porch/hack/create-deployment-blueprint.sh
@@ -126,14 +126,30 @@ function customize-image {
     "newTag=${TAG}"
 }
 
-function customize-image-in-command {
+function customize-image-in-env {
   local OLD="${1}"
   local NEW="${2}"
+  local TAG="${NEW##*:}"
+  local IMG="${NEW%:*}"
+
+  cat > set-image-config.yaml << EOF
+apiVersion: fn.kpt.dev/v1alpha1
+kind: SetImage
+metadata:
+  name: my-func-config
+image:
+  name: ${OLD}
+  newName: ${IMG}
+  newTag: ${TAG}
+additionalImageFields:
+- group: apps
+  version: v1
+  kind: Deployment
+  path: spec/template/spec/containers[]/env[]/value
+EOF
 
-  kpt fn eval "${DESTINATION}" --image search-replace:v0.2.0 -- \
-	  "by-path=spec.template.spec.containers[*].command[*]" \
-	  "by-value-regex=(.*)${OLD}" \
-	  "put-value=\${1}${NEW}"
+  kpt fn eval "${DESTINATION}" --image set-image:v0.1.0 --fn-config set-image-config.yaml
+  rm set-image-config.yaml
 }
 
 function customize-sa {
@@ -170,7 +186,7 @@ function main() {
   customize-image \
     "gcr.io/example-google-project-id/porch-controllers:latest" \
     "${CONTROLLERS_IMAGE}"
-  customize-image-in-command \
+  customize-image-in-env \
     "gcr.io/example-google-project-id/porch-wrapper-server:latest" \
     "${WRAPPER_SERVER_IMAGE}"
 
diff --git a/porch/repository/pkg/git/package.go b/porch/repository/pkg/git/package.go
index aeb7d428..8c59aceb 100644
--- a/porch/repository/pkg/git/package.go
+++ b/porch/repository/pkg/git/package.go
@@ -157,19 +157,28 @@ func (p *gitPackageRevision) GetUpstreamLock() (kptfile.Upstream, kptfile.Upstre
 		return kptfile.Upstream{}, kptfile.UpstreamLock{}, fmt.Errorf("cannot determine package lock: %w", err)
 	}
 
+	if p.ref == nil {
+		return kptfile.Upstream{}, kptfile.UpstreamLock{}, fmt.Errorf("cannot determine package lock; package has no ref")
+	}
+
+	ref, err := refInRemoteFromRefInLocal(p.ref.Name())
+	if err != nil {
+		return kptfile.Upstream{}, kptfile.UpstreamLock{}, fmt.Errorf("cannot determine package lock for %q: %v", p.ref, err)
+	}
+
 	return kptfile.Upstream{
 			Type: kptfile.GitOrigin,
 			Git: &kptfile.Git{
 				Repo:      repo,
 				Directory: p.path,
-				Ref:       p.revision,
+				Ref:       ref.Short(),
 			},
 		}, kptfile.UpstreamLock{
 			Type: kptfile.GitOrigin,
 			Git: &kptfile.GitLock{
 				Repo:      repo,
 				Directory: p.path,
-				Ref:       p.revision,
+				Ref:       ref.Short(),
 				Commit:    p.commit.String(),
 			},
 		}, nil


Test patch:
diff --git a/e2e/testdata/porch/rpkg-clone/config.yaml b/e2e/testdata/porch/rpkg-clone/config.yaml
index 2c4415eb..d235d975 100644
--- a/e2e/testdata/porch/rpkg-clone/config.yaml
+++ b/e2e/testdata/porch/rpkg-clone/config.yaml
@@ -109,14 +109,14 @@ commands:
         upstream:
           git:
             directory: empty
-            ref: v1
+            ref: empty/v1
             repo: https://github.com/platkrm/test-blueprints.git
           type: git
         upstreamLock:
           git:
             commit: 3de8635354eda8e7de756494a4e0eb5c12af01ab
             directory: empty
-            ref: v1
+            ref: empty/v1
             repo: https://github.com/platkrm/test-blueprints.git
           type: git
       kind: ResourceList
diff --git a/porch/apiserver/pkg/e2e/e2e_test.go b/porch/apiserver/pkg/e2e/e2e_test.go
index 8e57beb5..d926cc64 100644
--- a/porch/apiserver/pkg/e2e/e2e_test.go
+++ b/porch/apiserver/pkg/e2e/e2e_test.go
@@ -217,7 +217,7 @@ func (t *PorchSuite) TestCloneFromUpstream(ctx context.Context) {
 		Git: &kptfilev1.GitLock{
 			Repo:      testBlueprintsRepo,
 			Directory: "basens",
-			Ref:       "v1",
+			Ref:       "basens/v1",
 		},
 	}
 	if !cmp.Equal(want, got) {
@@ -230,7 +230,7 @@ func (t *PorchSuite) TestCloneFromUpstream(ctx context.Context) {
 		Git: &kptfilev1.Git{
 			Repo:      testBlueprintsRepo,
 			Directory: "basens",
-			Ref:       "v1",
+			Ref:       "basens/v1",
 		},
 	}); !cmp.Equal(want, got) {
 		t.Errorf("unexpected upstream returned (-want, +got) %s", cmp.Diff(want, got))
@@ -423,7 +423,7 @@ func (t *PorchSuite) TestCloneIntoDeploymentRepository(ctx context.Context) {
 		Git: &kptfilev1.GitLock{
 			Repo:      testBlueprintsRepo,
 			Directory: "basens",
-			Ref:       "v1",
+			Ref:       "basens/v1",
 		},
 	}
 	if !cmp.Equal(want, got) {
@@ -436,7 +436,7 @@ func (t *PorchSuite) TestCloneIntoDeploymentRepository(ctx context.Context) {
 		Git: &kptfilev1.Git{
 			Repo:      testBlueprintsRepo,
 			Directory: "basens",
-			Ref:       "v1",
+			Ref:       "basens/v1",
 		},
 	}); !cmp.Equal(want, got) {
 		t.Errorf("unexpected upstream returned (-want, +got) %s", cmp.Diff(want, got))
diff --git a/porch/repository/pkg/git/package_test.go b/porch/repository/pkg/git/package_test.go
new file mode 100644
index 00000000..b5f42960
--- /dev/null
+++ b/porch/repository/pkg/git/package_test.go
@@ -0,0 +1,126 @@
+// 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 git
+
+import (
+	"context"
+	"path/filepath"
+	"testing"
+
+	v1 "github.com/GoogleContainerTools/kpt/pkg/api/kptfile/v1"
+	"github.com/GoogleContainerTools/kpt/porch/api/porch/v1alpha1"
+	configapi "github.com/GoogleContainerTools/kpt/porch/api/porchconfig/v1alpha1"
+	"github.com/GoogleContainerTools/kpt/porch/repository/pkg/repository"
+	"github.com/go-git/go-git/v5/plumbing"
+	"github.com/google/go-cmp/cmp"
+)
+
+func (g GitSuite) TestUpstreamLock(t *testing.T) {
+	tempdir := t.TempDir()
+	tarfile := filepath.Join("testdata", "drafts-repository.tar")
+	repo, address := ServeGitRepositoryWithBranch(t, tarfile, tempdir, g.branch)
+
+	ctx := context.Background()
+	const (
+		repositoryName = "lock"
+		namespace      = "default"
+	)
+
+	git, err := OpenRepository(ctx, repositoryName, namespace, &configapi.GitRepository{
+		Repo:      address,
+		Branch:    g.branch,
+		Directory: "/",
+	}, tempdir, GitRepositoryOptions{})
+	if err != nil {
+		t.Fatalf("Failed to open Git repository loaded from %q: %v", tarfile, err)
+	}
+
+	revisions, err := git.ListPackageRevisions(ctx, repository.ListPackageRevisionFilter{})
+	if err != nil {
+		t.Fatalf("Failed to list packages from %q: %v", tarfile, err)
+	}
+
+	wantRefs := map[repository.PackageRevisionKey]string{
+		{Repository: repositoryName, Package: "empty", Revision: "v1"}:   "empty/v1",
+		{Repository: repositoryName, Package: "basens", Revision: "v1"}:  "basens/v1",
+		{Repository: repositoryName, Package: "basens", Revision: "v2"}:  "basens/v2",
+		{Repository: repositoryName, Package: "istions", Revision: "v1"}: "istions/v1",
+		{Repository: repositoryName, Package: "istions", Revision: "v2"}: "istions/v2",
+
+		{Repository: repositoryName, Package: "basens", Revision: g.branch}:  g.branch,
+		{Repository: repositoryName, Package: "empty", Revision: g.branch}:   g.branch,
+		{Repository: repositoryName, Package: "istions", Revision: g.branch}: g.branch,
+	}
+
+	for _, rev := range revisions {
+		if rev.Lifecycle() != v1alpha1.PackageRevisionLifecyclePublished {
+			continue
+		}
+
+		upstream, lock, err := rev.GetUpstreamLock()
+		if err != nil {
+			t.Errorf("GetUpstreamLock(%q) failed: %v", rev.Key(), err)
+		}
+		if got, want := upstream.Type, v1.GitOrigin; got != want {
+			t.Errorf("upstream.Type: got %s, want %s", got, want)
+		}
+		if got, want := lock.Type, v1.GitOrigin; got != want {
+			t.Errorf("lock.Type: got %s, want %s", got, want)
+		}
+
+		key := rev.Key()
+		wantRef, ok := wantRefs[key]
+		if !ok {
+			t.Errorf("Unexpected package found; %q", rev.Key())
+		}
+
+		type gitAddress struct {
+			Repo, Directory, Ref string
+		}
+
+		// Check upstream values
+		if got, want := (gitAddress{
+			Repo:      upstream.Git.Repo,
+			Directory: upstream.Git.Directory,
+			Ref:       upstream.Git.Ref,
+		}), (gitAddress{
+			Repo:      address,
+			Directory: key.Package,
+			Ref:       wantRef,
+		}); !cmp.Equal(want, got) {
+			t.Errorf("Package upstream differs (-want,+got): %s", cmp.Diff(want, got))
+		}
+
+		// Check upstream lock values
+		if got, want := (gitAddress{
+			Repo:      lock.Git.Repo,
+			Directory: lock.Git.Directory,
+			Ref:       lock.Git.Ref,
+		}), (gitAddress{
+			Repo:      address,
+			Directory: key.Package,
+			Ref:       wantRef,
+		}); !cmp.Equal(want, got) {
+			t.Errorf("Package upstream lock differs (-want,+got): %s", cmp.Diff(want, got))
+		}
+
+		// Check the commit
+		if commit, err := repo.ResolveRevision(plumbing.Revision(wantRef)); err != nil {
+			t.Errorf("ResolveRevision(%q) failed: %v", wantRef, err)
+		} else if got, want := lock.Git.Commit, commit.String(); got != want {
+			t.Errorf("Commit: got %s, want %s", got, want)
+		}
+	}
+}