Repository: akuity/kargo
Pull request #1903 (2024-04-29 09:45:10)

fix(api): use Unstructured to return raw response
Fixes: #1899

This pull request ensures that the raw object data returned by the Kargo API matches the data from the Kubernetes API server. Which, unlike the JSON and YAML serializers, purges empty structures.

netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy Preview for *docs-kargo-akuity-io* processing.


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 168b80dd161e5d239bf35421d58dc6a86a4c4973 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/docs-kargo-akuity-io/deploys/662f6c28f0e3a00008ae4c84 |

Patch:
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 7935831d..e992c1a9 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -100,7 +100,7 @@ jobs:
       id-token: write # for creating OIDC tokens for signing.
       packages: write # for uploading attestations. (https://github.com/slsa-framework/slsa-github-generator/blob/main/internal/builders/container/README.md#known-issues)
     # Must be refernced by a tag. https://github.com/slsa-framework/slsa-github-generator/blob/main/internal/builders/container/README.md#referencing-the-slsa-generator
-    uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v1.10.0
+    uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.0.0
     with:
       image: ${{ needs.publish-image.outputs.kargo-repo }}
       digest: ${{ needs.publish-image.outputs.image-digest }}
@@ -331,7 +331,7 @@ jobs:
       actions: read # To read the workflow path.
       id-token: write # To sign the provenance.
       contents: write # To add assets to a release.
-    uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.10.0
+    uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0
     with:
       base64-subjects: "${{ needs.combine_hashes.outputs.hashes }}"
       upload-assets: true # Optional: Upload to a new release
diff --git a/internal/api/get_analysisrun_v1alpha1.go b/internal/api/get_analysisrun_v1alpha1.go
index 7acecd3c..47e87cc3 100644
--- a/internal/api/get_analysisrun_v1alpha1.go
+++ b/internal/api/get_analysisrun_v1alpha1.go
@@ -6,8 +6,12 @@ import (
 	"fmt"
 
 	"connectrpc.com/connect"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/controller-runtime/pkg/client"
 
+	rolloutsapi "github.com/akuity/kargo/internal/controller/rollouts/api/v1alpha1"
 	svcv1alpha1 "github.com/akuity/kargo/pkg/api/service/v1alpha1"
 )
 
@@ -32,32 +36,50 @@ func (s *server) GetAnalysisRun(
 		return nil, err
 	}
 
-	ar, err := s.getAnalysisRunFn(ctx, s.client, types.NamespacedName{
+	// Get the AnalysisRun from the Kubernetes API as an unstructured object.
+	// Using an unstructured object allows us to return the object _as presented
+	// by the API_ if a raw format is requested.
+	u := unstructured.Unstructured{
+		Object: map[string]any{
+			"apiVersion": rolloutsapi.GroupVersion.String(),
+			"kind":       "AnalysisRun",
+		},
+	}
+	if err := s.client.Get(ctx, types.NamespacedName{
 		Namespace: namespace,
 		Name:      name,
-	})
-	if err != nil {
+	}, &u); err != nil {
+		if client.IgnoreNotFound(err) == nil {
+			err = fmt.Errorf("AnalysisRun %q not found in namespace %q", name, namespace)
+			return nil, connect.NewError(connect.CodeNotFound, err)
+		}
 		return nil, err
 	}
-	if ar == nil {
-		err = fmt.Errorf("AnalysisRun %q not found in namespace %q", name, namespace)
-		return nil, connect.NewError(connect.CodeNotFound, err)
-	}
 
-	obj, raw, err := objectOrRaw(ar, req.Msg.GetFormat())
-	if err != nil {
-		return nil, connect.NewError(connect.CodeInternal, err)
-	}
-	if raw != nil {
+	switch req.Msg.GetFormat() {
+	case svcv1alpha1.RawFormat_RAW_FORMAT_JSON, svcv1alpha1.RawFormat_RAW_FORMAT_YAML:
+		_, raw, err := objectOrRaw(&u, req.Msg.GetFormat())
+		if err != nil {
+			return nil, connect.NewError(connect.CodeInternal, err)
+		}
 		return connect.NewResponse(&svcv1alpha1.GetAnalysisRunResponse{
 			Result: &svcv1alpha1.GetAnalysisRunResponse_Raw{
 				Raw: raw,
 			},
 		}), nil
+	default:
+		ar := rolloutsapi.AnalysisRun{}
+		if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &ar); err != nil {
+			return nil, connect.NewError(connect.CodeInternal, err)
+		}
+		obj, _, err := objectOrRaw(&ar, req.Msg.GetFormat())
+		if err != nil {
+			return nil, connect.NewError(connect.CodeInternal, err)
+		}
+		return connect.NewResponse(&svcv1alpha1.GetAnalysisRunResponse{
+			Result: &svcv1alpha1.GetAnalysisRunResponse_AnalysisRun{
+				AnalysisRun: obj,
+			},
+		}), nil
 	}
-	return connect.NewResponse(&svcv1alpha1.GetAnalysisRunResponse{
-		Result: &svcv1alpha1.GetAnalysisRunResponse_AnalysisRun{
-			AnalysisRun: obj,
-		},
-	}), nil
 }
diff --git a/internal/api/get_analysistemplate_v1alpha1.go b/internal/api/get_analysistemplate_v1alpha1.go
index d3d6ec11..abaaddbd 100644
--- a/internal/api/get_analysistemplate_v1alpha1.go
+++ b/internal/api/get_analysistemplate_v1alpha1.go
@@ -6,8 +6,12 @@ import (
 	"fmt"
 
 	"connectrpc.com/connect"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/controller-runtime/pkg/client"
 
+	rolloutsapi "github.com/akuity/kargo/internal/controller/rollouts/api/v1alpha1"
 	svcv1alpha1 "github.com/akuity/kargo/pkg/api/service/v1alpha1"
 )
 
@@ -36,32 +40,50 @@ func (s *server) GetAnalysisTemplate(
 		return nil, err
 	}
 
-	at, err := s.getAnalysisTemplateFn(ctx, s.client, types.NamespacedName{
+	// Get the AnalysisTemplate from the Kubernetes API as an unstructured object.
+	// Using an unstructured object allows us to return the object _as presented
+	// by the API_ if a raw format is requested.
+	u := unstructured.Unstructured{
+		Object: map[string]any{
+			"apiVersion": rolloutsapi.GroupVersion.String(),
+			"kind":       "AnalysisTemplate",
+		},
+	}
+	if err := s.client.Get(ctx, types.NamespacedName{
 		Namespace: project,
 		Name:      name,
-	})
-	if err != nil {
+	}, &u); err != nil {
+		if client.IgnoreNotFound(err) == nil {
+			err = fmt.Errorf("AnalysisTemplate %q not found in namespace %q", name, project)
+			return nil, connect.NewError(connect.CodeNotFound, err)
+		}
 		return nil, err
 	}
-	if at == nil {
-		err = fmt.Errorf("AnalysisTemplate %q not found in namespace %q", name, project)
-		return nil, connect.NewError(connect.CodeNotFound, err)
-	}
 
-	obj, raw, err := objectOrRaw(at, req.Msg.GetFormat())
-	if err != nil {
-		return nil, connect.NewError(connect.CodeInternal, err)
-	}
-	if raw != nil {
+	switch req.Msg.GetFormat() {
+	case svcv1alpha1.RawFormat_RAW_FORMAT_JSON, svcv1alpha1.RawFormat_RAW_FORMAT_YAML:
+		_, raw, err := objectOrRaw(&u, req.Msg.GetFormat())
+		if err != nil {
+			return nil, connect.NewError(connect.CodeInternal, err)
+		}
 		return connect.NewResponse(&svcv1alpha1.GetAnalysisTemplateResponse{
 			Result: &svcv1alpha1.GetAnalysisTemplateResponse_Raw{
 				Raw: raw,
 			},
 		}), nil
+	default:
+		at := rolloutsapi.AnalysisTemplate{}
+		if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &at); err != nil {
+			return nil, connect.NewError(connect.CodeInternal, err)
+		}
+		obj, _, err := objectOrRaw(&at, req.Msg.GetFormat())
+		if err != nil {
+			return nil, connect.NewError(connect.CodeInternal, err)
+		}
+		return connect.NewResponse(&svcv1alpha1.GetAnalysisTemplateResponse{
+			Result: &svcv1alpha1.GetAnalysisTemplateResponse_AnalysisTemplate{
+				AnalysisTemplate: obj,
+			},
+		}), nil
 	}
-	return connect.NewResponse(&svcv1alpha1.GetAnalysisTemplateResponse{
-		Result: &svcv1alpha1.GetAnalysisTemplateResponse_AnalysisTemplate{
-			AnalysisTemplate: obj,
-		},
-	}), nil
 }
diff --git a/internal/api/get_freight_v1alpha1.go b/internal/api/get_freight_v1alpha1.go
index 9954b1b9..e0ac54ef 100644
--- a/internal/api/get_freight_v1alpha1.go
+++ b/internal/api/get_freight_v1alpha1.go
@@ -6,7 +6,12 @@ import (
 	"fmt"
 
 	"connectrpc.com/connect"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/controller-runtime/pkg/client"
 
+	kargoapi "github.com/akuity/kargo/api/v1alpha1"
 	svcv1alpha1 "github.com/akuity/kargo/pkg/api/service/v1alpha1"
 )
 
@@ -32,39 +37,78 @@ func (s *server) GetFreight(
 		return nil, err
 	}
 
-	freight, err := s.getFreightByNameOrAliasFn(
-		ctx,
-		s.client,
-		project,
-		name,
-		alias,
-	)
-	if err != nil {
-		return nil, fmt.Errorf("get freight: %w", err)
+	// Get the Freight from the Kubernetes API as an unstructured object.
+	// Using an unstructured object allows us to return the object _as presented
+	// by the API_ if a raw format is requested.
+	u := unstructured.Unstructured{
+		Object: map[string]any{
+			"apiVersion": kargoapi.GroupVersion.String(),
+			"kind":       "Freight",
+		},
 	}
-	if freight == nil {
-		if name != "" {
-			err = fmt.Errorf("freight %q not found in namespace %q", name, project)
-		} else {
-			err = fmt.Errorf("freight with alias %q not found in namespace %q", alias, project)
+
+	switch {
+	case alias != "":
+		ul := unstructured.UnstructuredList{
+			Object: map[string]any{
+				"apiVersion": kargoapi.GroupVersion.String(),
+				"kind":       "FreightList",
+			},
+		}
+		if err := s.client.List(
+			ctx,
+			&ul,
+			client.InNamespace(project),
+			client.MatchingLabels{
+				kargoapi.AliasLabelKey: alias,
+			},
+		); err != nil {
+			return nil, err
+		}
+		if len(ul.Items) == 0 {
+			return nil, connect.NewError(
+				connect.CodeNotFound,
+				fmt.Errorf("Freight with alias %q not found in namespace %q", alias, project),
+			)
+		}
+		u = ul.Items[0]
+	default:
+		if err := s.client.Get(ctx, types.NamespacedName{
+			Namespace: project,
+			Name:      name,
+		}, &u); err != nil {
+			if client.IgnoreNotFound(err) == nil {
+				err = fmt.Errorf("Freight %q not found in namespace %q", name, project)
+				return nil, connect.NewError(connect.CodeNotFound, err)
+			}
+			return nil, err
 		}
-		return nil, connect.NewError(connect.CodeNotFound, err)
 	}
 
-	obj, raw, err := objectOrRaw(freight, req.Msg.GetFormat())
-	if err != nil {
-		return nil, connect.NewError(connect.CodeInternal, err)
-	}
-	if raw != nil {
+	switch req.Msg.GetFormat() {
+	case svcv1alpha1.RawFormat_RAW_FORMAT_JSON, svcv1alpha1.RawFormat_RAW_FORMAT_YAML:
+		_, raw, err := objectOrRaw(&u, req.Msg.GetFormat())
+		if err != nil {
+			return nil, connect.NewError(connect.CodeInternal, err)
+		}
 		return connect.NewResponse(&svcv1alpha1.GetFreightResponse{
 			Result: &svcv1alpha1.GetFreightResponse_Raw{
 				Raw: raw,
 			},
 		}), nil
+	default:
+		f := kargoapi.Freight{}
+		if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &f); err != nil {
+			return nil, connect.NewError(connect.CodeInternal, err)
+		}
+		obj, _, err := objectOrRaw(&f, req.Msg.GetFormat())
+		if err != nil {
+			return nil, connect.NewError(connect.CodeInternal, err)
+		}
+		return connect.NewResponse(&svcv1alpha1.GetFreightResponse{
+			Result: &svcv1alpha1.GetFreightResponse_Freight{
+				Freight: obj,
+			},
+		}), nil
 	}
-	return connect.NewResponse(&svcv1alpha1.GetFreightResponse{
-		Result: &svcv1alpha1.GetFreightResponse_Freight{
-			Freight: obj,
-		},
-	}), nil
 }
diff --git a/internal/api/get_project_v1alpha1.go b/internal/api/get_project_v1alpha1.go
index af51399a..59b4c5f7 100644
--- a/internal/api/get_project_v1alpha1.go
+++ b/internal/api/get_project_v1alpha1.go
@@ -5,6 +5,8 @@ import (
 	"fmt"
 
 	"connectrpc.com/connect"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	kargoapi "github.com/akuity/kargo/api/v1alpha1"
@@ -20,30 +22,47 @@ func (s *server) GetProject(
 		return nil, err
 	}
 
-	var project kargoapi.Project
-	if err := s.client.Get(
-		ctx, client.ObjectKey{
-			Name: name,
+	// Get the Project from the Kubernetes API as an unstructured object.
+	// Using an unstructured object allows us to return the object _as presented
+	// by the API_ if a raw format is requested.
+	u := unstructured.Unstructured{
+		Object: map[string]any{
+			"apiVersion": kargoapi.GroupVersion.String(),
+			"kind":       "Project",
 		},
-		&project,
-	); err != nil {
-		return nil, fmt.Errorf("get project: %w", err)
 	}
-
-	obj, raw, err := objectOrRaw(&project, req.Msg.GetFormat())
-	if err != nil {
-		return nil, connect.NewError(connect.CodeInternal, err)
+	if err := s.client.Get(ctx, client.ObjectKey{Name: name}, &u); err != nil {
+		if client.IgnoreNotFound(err) == nil {
+			err = fmt.Errorf("Project %q not found", name)
+			return nil, connect.NewError(connect.CodeNotFound, err)
+		}
+		return nil, err
 	}
-	if raw != nil {
+
+	switch req.Msg.GetFormat() {
+	case svcv1alpha1.RawFormat_RAW_FORMAT_JSON, svcv1alpha1.RawFormat_RAW_FORMAT_YAML:
+		_, raw, err := objectOrRaw(&u, req.Msg.GetFormat())
+		if err != nil {
+			return nil, connect.NewError(connect.CodeInternal, err)
+		}
 		return connect.NewResponse(&svcv1alpha1.GetProjectResponse{
 			Result: &svcv1alpha1.GetProjectResponse_Raw{
 				Raw: raw,
 			},
 		}), nil
+	default:
+		p := kargoapi.Project{}
+		if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &p); err != nil {
+			return nil, connect.NewError(connect.CodeInternal, err)
+		}
+		obj, _, err := objectOrRaw(&p, req.Msg.GetFormat())
+		if err != nil {
+			return nil, connect.NewError(connect.CodeInternal, err)
+		}
+		return connect.NewResponse(&svcv1alpha1.GetProjectResponse{
+			Result: &svcv1alpha1.GetProjectResponse_Project{
+				Project: obj,
+			},
+		}), nil
 	}
-	return connect.NewResponse(&svcv1alpha1.GetProjectResponse{
-		Result: &svcv1alpha1.GetProjectResponse_Project{
-			Project: obj,
-		},
-	}), nil
 }
diff --git a/internal/api/get_promotion_v1alpha1.go b/internal/api/get_promotion_v1alpha1.go
index 89e5a415..473f6929 100644
--- a/internal/api/get_promotion_v1alpha1.go
+++ b/internal/api/get_promotion_v1alpha1.go
@@ -5,6 +5,8 @@ import (
 	"fmt"
 
 	"connectrpc.com/connect"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	kargoapi "github.com/akuity/kargo/api/v1alpha1"
@@ -29,28 +31,50 @@ func (s *server) GetPromotion(
 		return nil, err
 	}
 
-	var promotion kargoapi.Promotion
+	// Get the Promotion from the Kubernetes API as an unstructured object.
+	// Using an unstructured object allows us to return the object _as presented
+	// by the API_ if a raw format is requested.
+	u := unstructured.Unstructured{
+		Object: map[string]any{
+			"apiVersion": kargoapi.GroupVersion.String(),
+			"kind":       "Promotion",
+		},
+	}
 	if err := s.client.Get(ctx, client.ObjectKey{
-		Namespace: project,
 		Name:      name,
-	}, &promotion); err != nil {
-		return nil, fmt.Errorf("get promotion: %w", err)
+		Namespace: project,
+	}, &u); err != nil {
+		if client.IgnoreNotFound(err) == nil {
+			err = fmt.Errorf("Promotion %q not found in project %q", name, project)
+			return nil, connect.NewError(connect.CodeNotFound, err)
+		}
+		return nil, err
 	}
 
-	obj, raw, err := objectOrRaw(&promotion, req.Msg.GetFormat())
-	if err != nil {
-		return nil, connect.NewError(connect.CodeInternal, err)
-	}
-	if raw != nil {
+	switch req.Msg.GetFormat() {
+	case svcv1alpha1.RawFormat_RAW_FORMAT_JSON, svcv1alpha1.RawFormat_RAW_FORMAT_YAML:
+		_, raw, err := objectOrRaw(&u, req.Msg.GetFormat())
+		if err != nil {
+			return nil, connect.NewError(connect.CodeInternal, err)
+		}
 		return connect.NewResponse(&svcv1alpha1.GetPromotionResponse{
 			Result: &svcv1alpha1.GetPromotionResponse_Raw{
 				Raw: raw,
 			},
 		}), nil
+	default:
+		promotion := kargoapi.Promotion{}
+		if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &promotion); err != nil {
+			return nil, connect.NewError(connect.CodeInternal, err)
+		}
+		obj, _, err := objectOrRaw(&promotion, req.Msg.GetFormat())
+		if err != nil {
+			return nil, connect.NewError(connect.CodeInternal, err)
+		}
+		return connect.NewResponse(&svcv1alpha1.GetPromotionResponse{
+			Result: &svcv1alpha1.GetPromotionResponse_Promotion{
+				Promotion: obj,
+			},
+		}), nil
 	}
-	return connect.NewResponse(&svcv1alpha1.GetPromotionResponse{
-		Result: &svcv1alpha1.GetPromotionResponse_Promotion{
-			Promotion: obj,
-		},
-	}), nil
 }
diff --git a/internal/api/get_stage_v1alpha1.go b/internal/api/get_stage_v1alpha1.go
index 4cbb8f88..d7f79424 100644
--- a/internal/api/get_stage_v1alpha1.go
+++ b/internal/api/get_stage_v1alpha1.go
@@ -5,6 +5,8 @@ import (
 	"fmt"
 
 	"connectrpc.com/connect"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	kargoapi "github.com/akuity/kargo/api/v1alpha1"
@@ -29,28 +31,50 @@ func (s *server) GetStage(
 		return nil, err
 	}
 
-	var stage kargoapi.Stage
+	// Get the Stage from the Kubernetes API as an unstructured object.
+	// Using an unstructured object allows us to return the object _as presented
+	// by the API_ if a raw format is requested.
+	u := unstructured.Unstructured{
+		Object: map[string]any{
+			"apiVersion": kargoapi.GroupVersion.String(),
+			"kind":       "Stage",
+		},
+	}
 	if err := s.client.Get(ctx, client.ObjectKey{
-		Namespace: project,
 		Name:      name,
-	}, &stage); err != nil {
-		return nil, fmt.Errorf("get stage: %w", err)
+		Namespace: project,
+	}, &u); err != nil {
+		if client.IgnoreNotFound(err) == nil {
+			err = fmt.Errorf("Stage %q not found in project %q", name, project)
+			return nil, connect.NewError(connect.CodeNotFound, err)
+		}
+		return nil, err
 	}
 
-	obj, raw, err := objectOrRaw(&stage, req.Msg.GetFormat())
-	if err != nil {
-		return nil, connect.NewError(connect.CodeInternal, err)
-	}
-	if raw != nil {
+	switch req.Msg.GetFormat() {
+	case svcv1alpha1.RawFormat_RAW_FORMAT_JSON, svcv1alpha1.RawFormat_RAW_FORMAT_YAML:
+		_, raw, err := objectOrRaw(&u, req.Msg.GetFormat())
+		if err != nil {
+			return nil, connect.NewError(connect.CodeInternal, err)
+		}
 		return connect.NewResponse(&svcv1alpha1.GetStageResponse{
 			Result: &svcv1alpha1.GetStageResponse_Raw{
 				Raw: raw,
 			},
 		}), nil
+	default:
+		stage := kargoapi.Stage{}
+		if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &stage); err != nil {
+			return nil, connect.NewError(connect.CodeInternal, err)
+		}
+		obj, _, err := objectOrRaw(&stage, req.Msg.GetFormat())
+		if err != nil {
+			return nil, connect.NewError(connect.CodeInternal, err)
+		}
+		return connect.NewResponse(&svcv1alpha1.GetStageResponse{
+			Result: &svcv1alpha1.GetStageResponse_Stage{
+				Stage: obj,
+			},
+		}), nil
 	}
-	return connect.NewResponse(&svcv1alpha1.GetStageResponse{
-		Result: &svcv1alpha1.GetStageResponse_Stage{
-			Stage: obj,
-		},
-	}), nil
 }
diff --git a/internal/api/get_warehouse_v1alpha1.go b/internal/api/get_warehouse_v1alpha1.go
index d525f10c..3d996560 100644
--- a/internal/api/get_warehouse_v1alpha1.go
+++ b/internal/api/get_warehouse_v1alpha1.go
@@ -5,6 +5,8 @@ import (
 	"fmt"
 
 	"connectrpc.com/connect"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
 	kargoapi "github.com/akuity/kargo/api/v1alpha1"
@@ -29,28 +31,50 @@ func (s *server) GetWarehouse(
 		return nil, err
 	}
 
-	var warehouse kargoapi.Warehouse
+	// Get the Warehouse from the Kubernetes API as an unstructured object.
+	// Using an unstructured object allows us to return the object _as presented
+	// by the API_ if a raw format is requested.
+	u := unstructured.Unstructured{
+		Object: map[string]any{
+			"apiVersion": kargoapi.GroupVersion.String(),
+			"kind":       "Warehouse",
+		},
+	}
 	if err := s.client.Get(ctx, client.ObjectKey{
-		Namespace: project,
 		Name:      name,
-	}, &warehouse); err != nil {
-		return nil, fmt.Errorf("get warehouse: %w", err)
+		Namespace: project,
+	}, &u); err != nil {
+		if client.IgnoreNotFound(err) == nil {
+			err = fmt.Errorf("Warehouse %q not found in project %q", name, project)
+			return nil, connect.NewError(connect.CodeNotFound, err)
+		}
+		return nil, err
 	}
 
-	obj, raw, err := objectOrRaw(&warehouse, req.Msg.GetFormat())
-	if err != nil {
-		return nil, connect.NewError(connect.CodeInternal, err)
-	}
-	if raw != nil {
+	switch req.Msg.GetFormat() {
+	case svcv1alpha1.RawFormat_RAW_FORMAT_JSON, svcv1alpha1.RawFormat_RAW_FORMAT_YAML:
+		_, raw, err := objectOrRaw(&u, req.Msg.GetFormat())
+		if err != nil {
+			return nil, connect.NewError(connect.CodeInternal, err)
+		}
 		return connect.NewResponse(&svcv1alpha1.GetWarehouseResponse{
 			Result: &svcv1alpha1.GetWarehouseResponse_Raw{
 				Raw: raw,
 			},
 		}), nil
+	default:
+		w := kargoapi.Warehouse{}
+		if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &w); err != nil {
+			return nil, connect.NewError(connect.CodeInternal, err)
+		}
+		obj, _, err := objectOrRaw(&w, req.Msg.GetFormat())
+		if err != nil {
+			return nil, connect.NewError(connect.CodeInternal, err)
+		}
+		return connect.NewResponse(&svcv1alpha1.GetWarehouseResponse{
+			Result: &svcv1alpha1.GetWarehouseResponse_Warehouse{
+				Warehouse: obj,
+			},
+		}), nil
 	}
-	return connect.NewResponse(&svcv1alpha1.GetWarehouseResponse{
-		Result: &svcv1alpha1.GetWarehouseResponse_Warehouse{
-			Warehouse: obj,
-		},
-	}), nil
 }


Test patch:
diff --git a/internal/api/get_analysisrun_v1alpha1_test.go b/internal/api/get_analysisrun_v1alpha1_test.go
index 8deae4a9..015a7c84 100644
--- a/internal/api/get_analysisrun_v1alpha1_test.go
+++ b/internal/api/get_analysisrun_v1alpha1_test.go
@@ -10,10 +10,10 @@ import (
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/serializer"
-	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/client-go/rest"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+	"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
 
 	"github.com/akuity/kargo/internal/api/config"
 	"github.com/akuity/kargo/internal/api/kubernetes"
@@ -26,7 +26,7 @@ func TestGetAnalysisRun(t *testing.T) {
 	testCases := map[string]struct {
 		req              *svcv1alpha1.GetAnalysisRunRequest
 		rolloutsDisabled bool
-		getAnalysisRunFn func(context.Context, client.Client, types.NamespacedName) (*rollouts.AnalysisRun, error)
+		interceptor      interceptor.Funcs
 		assertions       func(*testing.T, *connect.Response[svcv1alpha1.GetAnalysisRunResponse], error)
 	}{
 		"empty namespace": {
@@ -34,7 +34,6 @@ func TestGetAnalysisRun(t *testing.T) {
 				Namespace: "",
 				Name:      "",
 			},
-			getAnalysisRunFn: rollouts.GetAnalysisRun,
 			assertions: func(t *testing.T, c *connect.Response[svcv1alpha1.GetAnalysisRunResponse], err error) {
 				require.Error(t, err)
 				require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
@@ -46,7 +45,6 @@ func TestGetAnalysisRun(t *testing.T) {
 				Namespace: "kargo-demo",
 				Name:      "",
 			},
-			getAnalysisRunFn: rollouts.GetAnalysisRun,
 			assertions: func(t *testing.T, c *connect.Response[svcv1alpha1.GetAnalysisRunResponse], err error) {
 				require.Error(t, err)
 				require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
@@ -58,7 +56,6 @@ func TestGetAnalysisRun(t *testing.T) {
 				Namespace: "kargo-demo",
 				Name:      "test",
 			},
-			getAnalysisRunFn: rollouts.GetAnalysisRun,
 			assertions: func(t *testing.T, c *connect.Response[svcv1alpha1.GetAnalysisRunResponse], err error) {
 				require.NoError(t, err)
 
@@ -75,7 +72,6 @@ func TestGetAnalysisRun(t *testing.T) {
 				Namespace: "kargo-x",
 				Name:      "test",
 			},
-			getAnalysisRunFn: rollouts.GetAnalysisRun,
 			assertions: func(t *testing.T, c *connect.Response[svcv1alpha1.GetAnalysisRunResponse], err error) {
 				require.Error(t, err)
 				require.Equal(t, connect.CodeNotFound, connect.CodeOf(err))
@@ -87,7 +83,6 @@ func TestGetAnalysisRun(t *testing.T) {
 				Namespace: "kargo-demo",
 				Name:      "non-existing",
 			},
-			getAnalysisRunFn: rollouts.GetAnalysisRun,
 			assertions: func(t *testing.T, c *connect.Response[svcv1alpha1.GetAnalysisRunResponse], err error) {
 				require.Error(t, err)
 				require.Equal(t, connect.CodeNotFound, connect.CodeOf(err))
@@ -99,8 +94,18 @@ func TestGetAnalysisRun(t *testing.T) {
 				Namespace: "kargo-demo",
 				Name:      "test",
 			},
-			getAnalysisRunFn: func(context.Context, client.Client, types.NamespacedName) (*rollouts.AnalysisRun, error) {
-				return nil, apierrors.NewServiceUnavailable("test")
+			interceptor: interceptor.Funcs{
+				// This interceptor will be called when the client.Get method is called.
+				// It will return an error to simulate a failure in the client.Get method.
+				Get: func(
+					_ context.Context,
+					_ client.WithWatch,
+					_ client.ObjectKey,
+					_ client.Object,
+					_ ...client.GetOption,
+				) error {
+					return apierrors.NewServiceUnavailable("test")
+				},
 			},
 			assertions: func(t *testing.T, c *connect.Response[svcv1alpha1.GetAnalysisRunResponse], err error) {
 				require.Error(t, err)
@@ -126,7 +131,6 @@ func TestGetAnalysisRun(t *testing.T) {
 				Name:      "test",
 				Format:    svcv1alpha1.RawFormat_RAW_FORMAT_JSON,
 			},
-			getAnalysisRunFn: rollouts.GetAnalysisRun,
 			assertions: func(t *testing.T, c *connect.Response[svcv1alpha1.GetAnalysisRunResponse], err error) {
 				require.NoError(t, err)
 
@@ -155,7 +159,6 @@ func TestGetAnalysisRun(t *testing.T) {
 				Name:      "test",
 				Format:    svcv1alpha1.RawFormat_RAW_FORMAT_YAML,
 			},
-			getAnalysisRunFn: rollouts.GetAnalysisRun,
 			assertions: func(t *testing.T, c *connect.Response[svcv1alpha1.GetAnalysisRunResponse], err error) {
 				require.NoError(t, err)
 
@@ -217,6 +220,7 @@ func TestGetAnalysisRun(t *testing.T) {
 								mustNewObject[corev1.Namespace]("testdata/namespace.yaml"),
 								mustNewObject[rollouts.AnalysisRun]("testdata/analysisrun.yaml"),
 							).
+							WithInterceptorFuncs(testCase.interceptor).
 							Build(), nil
 					},
 				},
@@ -224,9 +228,8 @@ func TestGetAnalysisRun(t *testing.T) {
 			require.NoError(t, err)
 
 			svr := &server{
-				cfg:              cfg,
-				client:           c,
-				getAnalysisRunFn: testCase.getAnalysisRunFn,
+				cfg:    cfg,
+				client: c,
 			}
 			res, err := (svr).GetAnalysisRun(ctx, connect.NewRequest(testCase.req))
 			testCase.assertions(t, res, err)
diff --git a/internal/api/get_analysistemplate_v1alpha1_test.go b/internal/api/get_analysistemplate_v1alpha1_test.go
index 78b77c80..2b6ec33a 100644
--- a/internal/api/get_analysistemplate_v1alpha1_test.go
+++ b/internal/api/get_analysistemplate_v1alpha1_test.go
@@ -10,10 +10,10 @@ import (
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/serializer"
-	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/client-go/rest"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+	"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
 
 	"github.com/akuity/kargo/internal/api/config"
 	"github.com/akuity/kargo/internal/api/kubernetes"
@@ -25,14 +25,10 @@ import (
 
 func TestGetAnalysisTemplate(t *testing.T) {
 	testCases := map[string]struct {
-		req                   *svcv1alpha1.GetAnalysisTemplateRequest
-		rolloutsDisabled      bool
-		getAnalysisTemplateFn func(
-			context.Context,
-			client.Client,
-			types.NamespacedName,
-		) (*rollouts.AnalysisTemplate, error)
-		assertions func(*testing.T, *connect.Response[svcv1alpha1.GetAnalysisTemplateResponse], error)
+		req              *svcv1alpha1.GetAnalysisTemplateRequest
+		rolloutsDisabled bool
+		interceptor      interceptor.Funcs
+		assertions       func(*testing.T, *connect.Response[svcv1alpha1.GetAnalysisTemplateResponse], error)
 	}{
 		"empty project": {
 			req: &svcv1alpha1.GetAnalysisTemplateRequest{
@@ -72,7 +68,6 @@ func TestGetAnalysisTemplate(t *testing.T) {
 				Project: "kargo-demo",
 				Name:    "test",
 			},
-			getAnalysisTemplateFn: rollouts.GetAnalysisTemplate,
 			assertions: func(t *testing.T, c *connect.Response[svcv1alpha1.GetAnalysisTemplateResponse], err error) {
 				require.NoError(t, err)
 
@@ -89,7 +84,6 @@ func TestGetAnalysisTemplate(t *testing.T) {
 				Project: "non-existing-project",
 				Name:    "test",
 			},
-			getAnalysisTemplateFn: rollouts.GetAnalysisTemplate,
 			assertions: func(t *testing.T, c *connect.Response[svcv1alpha1.GetAnalysisTemplateResponse], err error) {
 				require.Error(t, err)
 				require.Equal(t, connect.CodeNotFound, connect.CodeOf(err))
@@ -101,12 +95,18 @@ func TestGetAnalysisTemplate(t *testing.T) {
 				Project: "kargo-demo",
 				Name:    "test",
 			},
-			getAnalysisTemplateFn: func(
-				context.Context,
-				client.Client,
-				types.NamespacedName,
-			) (*rollouts.AnalysisTemplate, error) {
-				return nil, apierrors.NewServiceUnavailable("test")
+			interceptor: interceptor.Funcs{
+				// This interceptor will be called when the client.Get method is called.
+				// It will return an error to simulate a failure in the client.Get method.
+				Get: func(
+					_ context.Context,
+					_ client.WithWatch,
+					_ client.ObjectKey,
+					_ client.Object,
+					_ ...client.GetOption,
+				) error {
+					return apierrors.NewServiceUnavailable("test")
+				},
 			},
 			assertions: func(t *testing.T, c *connect.Response[svcv1alpha1.GetAnalysisTemplateResponse], err error) {
 				require.Error(t, err)
@@ -132,7 +132,6 @@ func TestGetAnalysisTemplate(t *testing.T) {
 				Name:    "test",
 				Format:  svcv1alpha1.RawFormat_RAW_FORMAT_JSON,
 			},
-			getAnalysisTemplateFn: rollouts.GetAnalysisTemplate,
 			assertions: func(t *testing.T, c *connect.Response[svcv1alpha1.GetAnalysisTemplateResponse], err error) {
 				require.NoError(t, err)
 
@@ -161,7 +160,6 @@ func TestGetAnalysisTemplate(t *testing.T) {
 				Name:    "test",
 				Format:  svcv1alpha1.RawFormat_RAW_FORMAT_YAML,
 			},
-			getAnalysisTemplateFn: rollouts.GetAnalysisTemplate,
 			assertions: func(t *testing.T, c *connect.Response[svcv1alpha1.GetAnalysisTemplateResponse], err error) {
 				require.NoError(t, err)
 
@@ -223,6 +221,7 @@ func TestGetAnalysisTemplate(t *testing.T) {
 								mustNewObject[corev1.Namespace]("testdata/namespace.yaml"),
 								mustNewObject[rollouts.AnalysisTemplate]("testdata/analysistemplate.yaml"),
 							).
+							WithInterceptorFuncs(testCase.interceptor).
 							Build(), nil
 					},
 				},
@@ -233,7 +232,6 @@ func TestGetAnalysisTemplate(t *testing.T) {
 				client:                    client,
 				cfg:                       cfg,
 				externalValidateProjectFn: validation.ValidateProject,
-				getAnalysisTemplateFn:     testCase.getAnalysisTemplateFn,
 			}
 			res, err := (svr).GetAnalysisTemplate(ctx, connect.NewRequest(testCase.req))
 			testCase.assertions(t, res, err)
diff --git a/internal/api/get_freight_v1alpha1_test.go b/internal/api/get_freight_v1alpha1_test.go
index 30ef4362..78f4b7be 100644
--- a/internal/api/get_freight_v1alpha1_test.go
+++ b/internal/api/get_freight_v1alpha1_test.go
@@ -14,6 +14,7 @@ import (
 	"k8s.io/client-go/rest"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+	"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
 
 	kargoapi "github.com/akuity/kargo/api/v1alpha1"
 	"github.com/akuity/kargo/internal/api/kubernetes"
@@ -24,16 +25,10 @@ import (
 
 func TestGetFreight(t *testing.T) {
 	testCases := map[string]struct {
-		req                       *svcv1alpha1.GetFreightRequest
-		objects                   []client.Object
-		getFreightByNameOrAliasFn func(
-			ctx context.Context,
-			c client.Client,
-			project string,
-			name string,
-			alias string,
-		) (*kargoapi.Freight, error)
-		assertions func(*testing.T, *connect.Response[svcv1alpha1.GetFreightResponse], error)
+		req         *svcv1alpha1.GetFreightRequest
+		objects     []client.Object
+		interceptor interceptor.Funcs
+		assertions  func(*testing.T, *connect.Response[svcv1alpha1.GetFreightResponse], error)
 	}{
 		"empty project": {
 			req: &svcv1alpha1.GetFreightRequest{
@@ -98,7 +93,6 @@ func TestGetFreight(t *testing.T) {
 				Project: "kargo-demo",
 				Name:    "test",
 			},
-			getFreightByNameOrAliasFn: kargoapi.GetFreightByNameOrAlias,
 			objects: []client.Object{
 				mustNewObject[corev1.Namespace]("testdata/namespace.yaml"),
 				&kargoapi.Freight{
@@ -124,7 +118,6 @@ func TestGetFreight(t *testing.T) {
 				Project: "kargo-demo",
 				Alias:   "test-alias",
 			},
-			getFreightByNameOrAliasFn: kargoapi.GetFreightByNameOrAlias,
 			objects: []client.Object{
 				mustNewObject[corev1.Namespace]("testdata/namespace.yaml"),
 				&kargoapi.Freight{
@@ -153,7 +146,6 @@ func TestGetFreight(t *testing.T) {
 				Project: "kargo-demo",
 				Name:    "non-existing",
 			},
-			getFreightByNameOrAliasFn: kargoapi.GetFreightByNameOrAlias,
 			objects: []client.Object{
 				mustNewObject[corev1.Namespace]("testdata/namespace.yaml"),
 			},
@@ -168,7 +160,6 @@ func TestGetFreight(t *testing.T) {
 				Project: "kargo-demo",
 				Alias:   "non-existing",
 			},
-			getFreightByNameOrAliasFn: kargoapi.GetFreightByNameOrAlias,
 			objects: []client.Object{
 				mustNewObject[corev1.Namespace]("testdata/namespace.yaml"),
 			},
@@ -183,14 +174,35 @@ func TestGetFreight(t *testing.T) {
 				Project: "kargo-demo",
 				Name:    "test",
 			},
-			getFreightByNameOrAliasFn: func(
-				context.Context,
-				client.Client,
-				string,
-				string,
-				string,
-			) (*kargoapi.Freight, error) {
-				return nil, apierrors.NewServiceUnavailable("test")
+			interceptor: interceptor.Funcs{
+				Get: func(
+					_ context.Context,
+					_ client.WithWatch,
+					_ client.ObjectKey,
+					_ client.Object,
+					_ ...client.GetOption,
+				) error {
+					return apierrors.NewServiceUnavailable("test")
+				},
+			},
+			objects: []client.Object{
+				mustNewObject[corev1.Namespace]("testdata/namespace.yaml"),
+			},
+			assertions: func(t *testing.T, c *connect.Response[svcv1alpha1.GetFreightResponse], err error) {
+				require.Error(t, err)
+				require.Equal(t, connect.CodeUnknown, connect.CodeOf(err))
+				require.Nil(t, c)
+			},
+		},
+		"error getting Freight by alias": {
+			req: &svcv1alpha1.GetFreightRequest{
+				Project: "kargo-demo",
+				Alias:   "test",
+			},
+			interceptor: interceptor.Funcs{
+				List: func(_ context.Context, _ client.WithWatch, _ client.ObjectList, _ ...client.ListOption) error {
+					return apierrors.NewServiceUnavailable("test")
+				},
 			},
 			objects: []client.Object{
 				mustNewObject[corev1.Namespace]("testdata/namespace.yaml"),
@@ -207,7 +219,6 @@ func TestGetFreight(t *testing.T) {
 				Name:    "test",
 				Format:  svcv1alpha1.RawFormat_RAW_FORMAT_JSON,
 			},
-			getFreightByNameOrAliasFn: kargoapi.GetFreightByNameOrAlias,
 			objects: []client.Object{
 				mustNewObject[corev1.Namespace]("testdata/namespace.yaml"),
 				&kargoapi.Freight{
@@ -249,7 +260,6 @@ func TestGetFreight(t *testing.T) {
 				Name:    "test",
 				Format:  svcv1alpha1.RawFormat_RAW_FORMAT_YAML,
 			},
-			getFreightByNameOrAliasFn: kargoapi.GetFreightByNameOrAlias,
 			objects: []client.Object{
 				mustNewObject[corev1.Namespace]("testdata/namespace.yaml"),
 				&kargoapi.Freight{
@@ -309,7 +319,7 @@ func TestGetFreight(t *testing.T) {
 						_ *rest.Config,
 						scheme *runtime.Scheme,
 					) (client.Client, error) {
-						c := fake.NewClientBuilder().WithScheme(scheme)
+						c := fake.NewClientBuilder().WithScheme(scheme).WithInterceptorFuncs(testCase.interceptor)
 						if len(testCase.objects) > 0 {
 							c.WithObjects(testCase.objects...)
 						}
@@ -322,7 +332,6 @@ func TestGetFreight(t *testing.T) {
 			svr := &server{
 				client:                    client,
 				externalValidateProjectFn: validation.ValidateProject,
-				getFreightByNameOrAliasFn: testCase.getFreightByNameOrAliasFn,
 			}
 			res, err := (svr).GetFreight(ctx, connect.NewRequest(testCase.req))
 			testCase.assertions(t, res, err)
diff --git a/internal/api/get_project_v1alpha1_test.go b/internal/api/get_project_v1alpha1_test.go
index cc37ee33..a3d8cd81 100644
--- a/internal/api/get_project_v1alpha1_test.go
+++ b/internal/api/get_project_v1alpha1_test.go
@@ -6,12 +6,14 @@ import (
 
 	"connectrpc.com/connect"
 	"github.com/stretchr/testify/require"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/serializer"
 	"k8s.io/client-go/rest"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+	"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
 
 	kargoapi "github.com/akuity/kargo/api/v1alpha1"
 	"github.com/akuity/kargo/internal/api/kubernetes"
@@ -21,9 +23,10 @@ import (
 
 func TestGetProject(t *testing.T) {
 	testCases := map[string]struct {
-		req        *svcv1alpha1.GetProjectRequest
-		objects    []client.Object
-		assertions func(*testing.T, *connect.Response[svcv1alpha1.GetProjectResponse], error)
+		req         *svcv1alpha1.GetProjectRequest
+		objects     []client.Object
+		interceptor interceptor.Funcs
+		assertions  func(*testing.T, *connect.Response[svcv1alpha1.GetProjectResponse], error)
 	}{
 		"empty name": {
 			req: &svcv1alpha1.GetProjectRequest{
@@ -47,6 +50,30 @@ func TestGetProject(t *testing.T) {
 				Name: "kargo-x",
 			},
 			objects: []client.Object{},
+			assertions: func(t *testing.T, c *connect.Response[svcv1alpha1.GetProjectResponse], err error) {
+				require.Error(t, err)
+				require.Equal(t, connect.CodeNotFound, connect.CodeOf(err))
+				require.Nil(t, c)
+			},
+		},
+		"error getting Project": {
+			req: &svcv1alpha1.GetProjectRequest{
+				Name: "kargo-demo",
+			},
+			objects: []client.Object{},
+			interceptor: interceptor.Funcs{
+				// This interceptor will be called when the client.Get method is called.
+				// It will return an error to simulate a failure in the client.Get method.
+				Get: func(
+					_ context.Context,
+					_ client.WithWatch,
+					_ client.ObjectKey,
+					_ client.Object,
+					_ ...client.GetOption,
+				) error {
+					return apierrors.NewServiceUnavailable("test")
+				},
+			},
 			assertions: func(t *testing.T, c *connect.Response[svcv1alpha1.GetProjectResponse], err error) {
 				require.Error(t, err)
 				require.Equal(t, connect.CodeUnknown, connect.CodeOf(err))
@@ -172,7 +199,7 @@ func TestGetProject(t *testing.T) {
 						_ *rest.Config,
 						scheme *runtime.Scheme,
 					) (client.Client, error) {
-						c := fake.NewClientBuilder().WithScheme(scheme)
+						c := fake.NewClientBuilder().WithScheme(scheme).WithInterceptorFuncs(testCase.interceptor)
 						if len(testCase.objects) > 0 {
 							c.WithObjects(testCase.objects...)
 						}
diff --git a/internal/api/get_promotion_v1alpha1_test.go b/internal/api/get_promotion_v1alpha1_test.go
index 20c27cf6..fb754f80 100644
--- a/internal/api/get_promotion_v1alpha1_test.go
+++ b/internal/api/get_promotion_v1alpha1_test.go
@@ -7,12 +7,14 @@ import (
 	"connectrpc.com/connect"
 	"github.com/stretchr/testify/require"
 	corev1 "k8s.io/api/core/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/serializer"
 	"k8s.io/client-go/rest"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+	"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
 
 	kargoapi "github.com/akuity/kargo/api/v1alpha1"
 	"github.com/akuity/kargo/internal/api/kubernetes"
@@ -23,9 +25,10 @@ import (
 
 func TestGetPromotion(t *testing.T) {
 	testCases := map[string]struct {
-		req        *svcv1alpha1.GetPromotionRequest
-		objects    []client.Object
-		assertions func(*testing.T, *connect.Response[svcv1alpha1.GetPromotionResponse], error)
+		req         *svcv1alpha1.GetPromotionRequest
+		objects     []client.Object
+		interceptor interceptor.Funcs
+		assertions  func(*testing.T, *connect.Response[svcv1alpha1.GetPromotionResponse], error)
 	}{
 		"empty project": {
 			req: &svcv1alpha1.GetPromotionRequest{
@@ -77,6 +80,33 @@ func TestGetPromotion(t *testing.T) {
 			objects: []client.Object{
 				mustNewObject[corev1.Namespace]("testdata/namespace.yaml"),
 			},
+			assertions: func(t *testing.T, c *connect.Response[svcv1alpha1.GetPromotionResponse], err error) {
+				require.Error(t, err)
+				require.Equal(t, connect.CodeNotFound, connect.CodeOf(err))
+				require.Nil(t, c)
+			},
+		},
+		"error getting Promotion": {
+			req: &svcv1alpha1.GetPromotionRequest{
+				Project: "kargo-demo",
+				Name:    "test",
+			},
+			objects: []client.Object{
+				mustNewObject[corev1.Namespace]("testdata/namespace.yaml"),
+			},
+			interceptor: interceptor.Funcs{
+				// This interceptor will be called when the client.Get method is called.
+				// It will return an error to simulate a failure in the client.Get method.
+				Get: func(
+					_ context.Context,
+					_ client.WithWatch,
+					_ client.ObjectKey,
+					_ client.Object,
+					_ ...client.GetOption,
+				) error {
+					return apierrors.NewServiceUnavailable("test")
+				},
+			},
 			assertions: func(t *testing.T, c *connect.Response[svcv1alpha1.GetPromotionResponse], err error) {
 				require.Error(t, err)
 				require.Equal(t, connect.CodeUnknown, connect.CodeOf(err))
@@ -214,7 +244,7 @@ func TestGetPromotion(t *testing.T) {
 						_ *rest.Config,
 						scheme *runtime.Scheme,
 					) (client.Client, error) {
-						c := fake.NewClientBuilder().WithScheme(scheme)
+						c := fake.NewClientBuilder().WithScheme(scheme).WithInterceptorFuncs(testCase.interceptor)
 						if len(testCase.objects) > 0 {
 							c.WithObjects(testCase.objects...)
 						}
diff --git a/internal/api/get_stage_v1alpha1_test.go b/internal/api/get_stage_v1alpha1_test.go
index a2bcf858..967055e4 100644
--- a/internal/api/get_stage_v1alpha1_test.go
+++ b/internal/api/get_stage_v1alpha1_test.go
@@ -7,11 +7,13 @@ import (
 	"connectrpc.com/connect"
 	"github.com/stretchr/testify/require"
 	corev1 "k8s.io/api/core/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/serializer"
 	"k8s.io/client-go/rest"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+	"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
 
 	kargoapi "github.com/akuity/kargo/api/v1alpha1"
 	"github.com/akuity/kargo/internal/api/kubernetes"
@@ -22,8 +24,9 @@ import (
 
 func TestGetStage(t *testing.T) {
 	testSets := map[string]struct {
-		req        *svcv1alpha1.GetStageRequest
-		assertions func(*testing.T, *connect.Response[svcv1alpha1.GetStageResponse], error)
+		req         *svcv1alpha1.GetStageRequest
+		interceptor interceptor.Funcs
+		assertions  func(*testing.T, *connect.Response[svcv1alpha1.GetStageResponse], error)
 	}{
 		"empty project": {
 			req: &svcv1alpha1.GetStageRequest{
@@ -79,6 +82,28 @@ func TestGetStage(t *testing.T) {
 				Project: "kargo-demo",
 				Name:    "non-existing",
 			},
+			assertions: func(t *testing.T, c *connect.Response[svcv1alpha1.GetStageResponse], err error) {
+				require.Error(t, err)
+				require.Equal(t, connect.CodeNotFound, connect.CodeOf(err))
+				require.Nil(t, c)
+			},
+		},
+		"error getting Stage": {
+			req: &svcv1alpha1.GetStageRequest{
+				Project: "kargo-demo",
+				Name:    "test",
+			},
+			interceptor: interceptor.Funcs{
+				Get: func(
+					_ context.Context,
+					_ client.WithWatch,
+					_ client.ObjectKey,
+					_ client.Object,
+					_ ...client.GetOption,
+				) error {
+					return apierrors.NewServiceUnavailable("test")
+				},
+			},
 			assertions: func(t *testing.T, c *connect.Response[svcv1alpha1.GetStageResponse], err error) {
 				require.Error(t, err)
 				require.Equal(t, connect.CodeUnknown, connect.CodeOf(err))
@@ -171,6 +196,7 @@ func TestGetStage(t *testing.T) {
 								mustNewObject[corev1.Namespace]("testdata/namespace.yaml"),
 								mustNewObject[kargoapi.Stage]("testdata/stage.yaml"),
 							).
+							WithInterceptorFuncs(ts.interceptor).
 							Build(), nil
 					},
 				},
diff --git a/internal/api/get_warehouse_v1alpha1_test.go b/internal/api/get_warehouse_v1alpha1_test.go
index c288cf43..a047ff64 100644
--- a/internal/api/get_warehouse_v1alpha1_test.go
+++ b/internal/api/get_warehouse_v1alpha1_test.go
@@ -7,12 +7,14 @@ import (
 	"connectrpc.com/connect"
 	"github.com/stretchr/testify/require"
 	corev1 "k8s.io/api/core/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/serializer"
 	"k8s.io/client-go/rest"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+	"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
 
 	kargoapi "github.com/akuity/kargo/api/v1alpha1"
 	"github.com/akuity/kargo/internal/api/kubernetes"
@@ -23,9 +25,10 @@ import (
 
 func TestGetWarehouse(t *testing.T) {
 	testSets := map[string]struct {
-		req        *svcv1alpha1.GetWarehouseRequest
-		objects    []client.Object
-		assertions func(*testing.T, *connect.Response[svcv1alpha1.GetWarehouseResponse], error)
+		req         *svcv1alpha1.GetWarehouseRequest
+		objects     []client.Object
+		interceptor interceptor.Funcs
+		assertions  func(*testing.T, *connect.Response[svcv1alpha1.GetWarehouseResponse], error)
 	}{
 		"empty project": {
 			req: &svcv1alpha1.GetWarehouseRequest{
@@ -102,6 +105,33 @@ func TestGetWarehouse(t *testing.T) {
 			objects: []client.Object{
 				mustNewObject[corev1.Namespace]("testdata/namespace.yaml"),
 			},
+			assertions: func(t *testing.T, c *connect.Response[svcv1alpha1.GetWarehouseResponse], err error) {
+				require.Error(t, err)
+				require.Equal(t, connect.CodeNotFound, connect.CodeOf(err))
+				require.Nil(t, c)
+			},
+		},
+		"error getting Warehouse": {
+			req: &svcv1alpha1.GetWarehouseRequest{
+				Project: "kargo-demo",
+				Name:    "test",
+			},
+			objects: []client.Object{
+				mustNewObject[corev1.Namespace]("testdata/namespace.yaml"),
+			},
+			interceptor: interceptor.Funcs{
+				// This interceptor will be called when the client.Get method is called.
+				// It will return an error to simulate a failure in the client.Get method.
+				Get: func(
+					_ context.Context,
+					_ client.WithWatch,
+					_ client.ObjectKey,
+					_ client.Object,
+					_ ...client.GetOption,
+				) error {
+					return apierrors.NewServiceUnavailable("test")
+				},
+			},
 			assertions: func(t *testing.T, c *connect.Response[svcv1alpha1.GetWarehouseResponse], err error) {
 				require.Error(t, err)
 				require.Equal(t, connect.CodeUnknown, connect.CodeOf(err))
@@ -215,7 +245,7 @@ func TestGetWarehouse(t *testing.T) {
 						_ *rest.Config,
 						scheme *runtime.Scheme,
 					) (client.Client, error) {
-						c := fake.NewClientBuilder().WithScheme(scheme)
+						c := fake.NewClientBuilder().WithScheme(scheme).WithInterceptorFuncs(ts.interceptor)
 						if ts.objects != nil {
 							c.WithObjects(ts.objects...)
 						}