Repository: akuity/kargo
Pull request #1690 (2024-03-27 09:02:29)

refactor(api): move rollouts setting to server cfg
Follow up on https://github.com/akuity/kargo/pull/1682#discussion_r1540008282

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


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 3846aaf10798c7e7979eb88e28503ab50900e443 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/docs-kargo-akuity-io/deploys/6603e0a72fc6960008cb466c |
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/akuity/kargo/pull/1690?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity) Report
Attention: Patch coverage is `28.57143%` with `5 lines` in your changes are missing coverage. Please review.
> Project coverage is 43.94%. Comparing base [(`a492ced`)](https://app.codecov.io/gh/akuity/kargo/commit/a492ced810fe024af08b30fa7dea2c7ab7ca4b2d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity) to head [(`3846aaf`)](https://app.codecov.io/gh/akuity/kargo/pull/1690?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity).

| [Files](https://app.codecov.io/gh/akuity/kargo/pull/1690?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity) | Patch % | Lines |
|---|---|---|
| [cmd/controlplane/api.go](https://app.codecov.io/gh/akuity/kargo/pull/1690?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity#diff-Y21kL2NvbnRyb2xwbGFuZS9hcGkuZ28=) | 0.00% | [3 Missing :warning: ](https://app.codecov.io/gh/akuity/kargo/pull/1690?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity) |
| [internal/api/config/config.go](https://app.codecov.io/gh/akuity/kargo/pull/1690?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity#diff-aW50ZXJuYWwvYXBpL2NvbmZpZy9jb25maWcuZ28=) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/akuity/kargo/pull/1690?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity) |

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main    #1690   +/-   ##
=======================================
  Coverage   43.93%   43.94%           
=======================================
  Files         203      203           
  Lines       13061    13058    -3     
=======================================
- Hits         5739     5738    -1     
+ Misses       7075     7073    -2     
  Partials      247      247           
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/akuity/kargo/pull/1690?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity).


Patch:
diff --git a/charts/kargo/README.md b/charts/kargo/README.md
index 90986425..4a58ca8b 100644
--- a/charts/kargo/README.md
+++ b/charts/kargo/README.md
@@ -70,6 +70,7 @@ the Kargo controller is running.
 | `api.tolerations`                           | Tolerations for api pods.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | `[]`                     |
 | `api.affinity`                              | Specifies pod affinity for api pods.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | `{}`                     |
 | `api.annotations`                           | Annotations to add to the api pods.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | `{}`                     |
+| `api.securityContext`                       | Security context for api pods.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | `{}`                     |
 | `api.probes.enabled`                        | Whether liveness and readiness probes should be included in the API server deployment. It is sometimes advantageous to disable these during local development.                                                                                                                                                                                                                                                                                                                                                                  | `true`                   |
 | `api.tls.enabled`                           | Whether to enable TLS directly on the API server. This is helpful if you do not intend to use an ingress controller or if you require TLS end-to-end. All other settings in this section will be ignored when this is set to `false`.                                                                                                                                                                                                                                                                                           | `true`                   |
 | `api.tls.selfSignedCert`                    | Whether to generate a self-signed certificate for use by the API server. If `true`, `cert-manager` CRDs **must** be present in the cluster. Kargo will create and use its own namespaced issuer. If `false`, a cert secret named `kargo-api-cert` **must** be provided in the same namespace as Kargo.                                                                                                                                                                                                                          | `true`                   |
@@ -113,6 +114,7 @@ the Kargo controller is running.
 | `api.oidc.dex.tolerations`                  | Tolerations for Dex server pods.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | `[]`                     |
 | `api.oidc.dex.affinity`                     | Specifies pod affinity for the Dex server pods.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | `{}`                     |
 | `api.oidc.dex.annotations`                  | Annotations to add to the Dex server pods.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `{}`                     |
+| `api.oidc.dex.securityContext`              | Security context for Dex server pods.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           | `{}`                     |
 | `api.argocd.urls`                           | Mapping of Argo CD shards names to URLs to support deep links to Argo CD URLs. If sharding is not used, map the empty string to the single Argo CD URL.                                                                                                                                                                                                                                                                                                                                                                         | `nil`                    |
 | `api.rollouts.integrationEnabled`           | Specifies whether Argo Rollouts integration is enabled. When not enabled, the API server will not be capable of creating/updating/applying AnalysesTemplate resources in the Kargo control plane. When enabled, the API server will perform a sanity check at startup. If Argo Rollouts CRDs are not found, the API server will proceed as if this integration had been explicitly disabled. Explicitly disabling is still preferable if this integration is not desired, as it will grant fewer permissions to the API server. | `true`                   |
 
@@ -122,6 +124,7 @@ the Kargo controller is running.
 | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- |
 | `controller.enabled`                         | Whether the controller is enabled.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | `true`      |
 | `controller.globalCredentials.namespaces`    | List of namespaces to look for shared credentials.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | `[]`        |
+| `controller.securityContext`                 | Security context for controller pods.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | `{}`        |
 | `controller.shardName`                       | Set a shard name only if you are running multiple controllers backed by a single underlying control plane. Setting a shard name will cause this controller to operate **only** on resources with a matching shard name. Leaving the shard name undefined will designate this controller as the default controller that is responsible exclusively for resources that are **not** assigned to a specific shard. Leaving this undefined is the correct choice when you are not using sharding at all. It is also the correct setting if you are using sharding and want to designate a controller as the default for handling resources not assigned to a specific shard. In most cases, this setting should simply be left alone. | `undefined` |
 | `controller.argocd.integrationEnabled`       | Specifies whether Argo CD integration is enabled. When not enabled, the controller will not watch Argo CD Application resources or factor Application health and sync state into determinations of Stage health. Argo CD-based promotion mechanisms will also fail. When enabled, the controller will perform a sanity check at startup. If Argo CD CRDs are not found, the controller will proceed as if this integration had been explicitly disabled. Explicitly disabling is still preferable if this integration is not desired, as it will grant fewer permissions to the controller.                                                                                                                                      | `true`      |
 | `controller.argocd.namespace`                | The namespace into which Argo CD is installed.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   | `argocd`    |
@@ -138,15 +141,16 @@ the Kargo controller is running.
 
 ### Management Controller
 
-| Name                                | Description                                                             | Value  |
-| ----------------------------------- | ----------------------------------------------------------------------- | ------ |
-| `managementController.enabled`      | Whether the management controller is enabled.                           | `true` |
-| `managementController.logLevel`     | The log level for the management controller.                            | `INFO` |
-| `managementController.resources`    | Resources limits and requests for the management controller containers. | `{}`   |
-| `managementController.nodeSelector` | Node selector for management controller pods.                           | `{}`   |
-| `managementController.tolerations`  | Tolerations for management controller pods.                             | `[]`   |
-| `managementController.affinity`     | Specifies pod affinity for management controller pods.                  | `{}`   |
-| `managementController.annotations`  | Annotations to add to the management controller pods.                   | `{}`   |
+| Name                                   | Description                                                             | Value  |
+| -------------------------------------- | ----------------------------------------------------------------------- | ------ |
+| `managementController.enabled`         | Whether the management controller is enabled.                           | `true` |
+| `managementController.logLevel`        | The log level for the management controller.                            | `INFO` |
+| `managementController.securityContext` | Security context for management controller pods.                        | `{}`   |
+| `managementController.resources`       | Resources limits and requests for the management controller containers. | `{}`   |
+| `managementController.nodeSelector`    | Node selector for management controller pods.                           | `{}`   |
+| `managementController.tolerations`     | Tolerations for management controller pods.                             | `[]`   |
+| `managementController.affinity`        | Specifies pod affinity for management controller pods.                  | `{}`   |
+| `managementController.annotations`     | Annotations to add to the management controller pods.                   | `{}`   |
 
 ### Webhooks
 
@@ -167,6 +171,7 @@ the Kargo controller is running.
 | `webhooksServer.tolerations`        | Tolerations for the webhooks server pods.                                                                                                                                                                                                                                                                                                                                             | `[]`   |
 | `webhooksServer.affinity`           | Specifies pod affinity for the webhooks server pods.                                                                                                                                                                                                                                                                                                                                  | `{}`   |
 | `webhooksServer.annotations`        | Annotations to add to the webhooks server pods.                                                                                                                                                                                                                                                                                                                                       | `{}`   |
+| `webhooksServer.securityContext`    | Security context for webhooks server pods.                                                                                                                                                                                                                                                                                                                                            | `{}`   |
 
 ### Garbage Collector
 
@@ -185,3 +190,5 @@ the Kargo controller is running.
 | `garbageCollector.tolerations`             | Tolerations for the garbage collector pods.                                                                                                                                                                                                                                                                               | `[]`        |
 | `garbageCollector.affinity`                | Specifies pod affinity for the garbage collector pods.                                                                                                                                                                                                                                                                    | `{}`        |
 | `garbageCollector.annotations`             | Annotations to add to the garbage collector pods.                                                                                                                                                                                                                                                                         | `{}`        |
+| `garbageCollector.securityContext`         | Security context for garbage collector pods.                                                                                                                                                                                                                                                                              | `{}`        |
+
diff --git a/charts/kargo/templates/api/deployment.yaml b/charts/kargo/templates/api/deployment.yaml
index 7ad5139f..1a65a414 100644
--- a/charts/kargo/templates/api/deployment.yaml
+++ b/charts/kargo/templates/api/deployment.yaml
@@ -79,6 +79,8 @@ spec:
               name: config
               readOnly: true
 {{- end }}
+          securityContext:
+            {{- toYaml .Values.api.securityContext | nindent 12 }}
           resources:
             {{- toYaml .Values.api.resources | nindent 12 }}
 {{- if or .Values.kubeconfigSecrets.kargo (and .Values.api.oidc.enabled .Values.api.oidc.dex.enabled) .Values.api.tls.enabled }}
diff --git a/charts/kargo/templates/controller/deployment.yaml b/charts/kargo/templates/controller/deployment.yaml
index 59cf91c6..41d04681 100644
--- a/charts/kargo/templates/controller/deployment.yaml
+++ b/charts/kargo/templates/controller/deployment.yaml
@@ -50,6 +50,8 @@ spec:
           name: kubeconfigs
           readOnly: true
         {{- end }}
+        securityContext:
+          {{- toYaml .Values.controller.securityContext | nindent 10 }}
         resources:
           {{- toYaml .Values.controller.resources | nindent 10 }}
       {{- if or .Values.kubeconfigSecrets.kargo .Values.kubeconfigSecrets.argocd .Values.kubeconfigSecrets.rollouts }}
diff --git a/charts/kargo/templates/dex-server/deployment.yaml b/charts/kargo/templates/dex-server/deployment.yaml
index 05783f3f..5e9b4dd3 100644
--- a/charts/kargo/templates/dex-server/deployment.yaml
+++ b/charts/kargo/templates/dex-server/deployment.yaml
@@ -48,6 +48,8 @@ spec:
         - mountPath: /etc/dex
           name: config
           readOnly: true
+        securityContext:
+          {{- toYaml .Values.api.oidc.dex.securityContext | nindent 10 }}
         resources:
           {{- toYaml .Values.api.oidc.dex.resources | nindent 10 }}
         {{- if .Values.api.oidc.dex.probes.enabled }}
diff --git a/charts/kargo/templates/garbage-collector/cron-job.yaml b/charts/kargo/templates/garbage-collector/cron-job.yaml
index afb77cf2..fc65b025 100644
--- a/charts/kargo/templates/garbage-collector/cron-job.yaml
+++ b/charts/kargo/templates/garbage-collector/cron-job.yaml
@@ -37,6 +37,8 @@ spec:
             envFrom:
             - configMapRef:
                 name: kargo-garbage-collector
+            securityContext:
+              {{- toYaml .Values.garbageCollector.securityContext | nindent 14 }}
             resources:
               {{- toYaml .Values.garbageCollector.resources | nindent 14 }}
           restartPolicy: Never
diff --git a/charts/kargo/templates/management-controller/deployment.yaml b/charts/kargo/templates/management-controller/deployment.yaml
index bf95220a..1fd82896 100644
--- a/charts/kargo/templates/management-controller/deployment.yaml
+++ b/charts/kargo/templates/management-controller/deployment.yaml
@@ -50,6 +50,8 @@ spec:
           name: kubeconfigs
           readOnly: true
         {{- end }}
+        securityContext:
+          {{- toYaml .Values.managementController.securityContext | nindent 10 }}
         resources:
           {{- toYaml .Values.managementController.resources | nindent 10 }}
       {{- if .Values.kubeconfigSecrets.kargo }}
diff --git a/charts/kargo/templates/webhooks-server/deployment.yaml b/charts/kargo/templates/webhooks-server/deployment.yaml
index 139a5056..0607173f 100644
--- a/charts/kargo/templates/webhooks-server/deployment.yaml
+++ b/charts/kargo/templates/webhooks-server/deployment.yaml
@@ -57,6 +57,8 @@ spec:
           name: kubeconfigs
           readOnly: true
         {{- end }}
+        securityContext:
+          {{- toYaml .Values.webhooksServer.securityContext | nindent 10 }}
         resources:
           {{- toYaml .Values.webhooksServer.resources | nindent 10 }}
       volumes:
diff --git a/charts/kargo/values.yaml b/charts/kargo/values.yaml
index 0cc78dc8..b085fac8 100755
--- a/charts/kargo/values.yaml
+++ b/charts/kargo/values.yaml
@@ -88,6 +88,8 @@ api:
   affinity: {}
   ## @param  api.annotations Annotations to add to the api pods.
   annotations: {}
+  ## @param api.securityContext Security context for api pods.
+  securityContext: {}
 
   probes:
     ## @param api.probes.enabled Whether liveness and readiness probes should be included in the API server deployment. It is sometimes advantageous to disable these during local development.
@@ -243,6 +245,8 @@ api:
       affinity: {}
       ## @param api.oidc.dex.annotations Annotations to add to the Dex server pods.
       annotations: {}
+      ## @param api.oidc.dex.securityContext Security context for Dex server pods.
+      securityContext: {}
 
   argocd:
     ## @param api.argocd.urls Mapping of Argo CD shards names to URLs to support deep links to Argo CD URLs. If sharding is not used, map the empty string to the single Argo CD URL.
@@ -266,6 +270,9 @@ controller:
     ## @param controller.globalCredentials.namespaces List of namespaces to look for shared credentials.
     namespaces: []
 
+  ## @param controller.securityContext Security context for controller pods.
+  securityContext: {}
+
   ## @param controller.shardName [nullable] Set a shard name only if you are running multiple controllers backed by a single underlying control plane. Setting a shard name will cause this controller to operate **only** on resources with a matching shard name. Leaving the shard name undefined will designate this controller as the default controller that is responsible exclusively for resources that are **not** assigned to a specific shard. Leaving this undefined is the correct choice when you are not using sharding at all. It is also the correct setting if you are using sharding and want to designate a controller as the default for handling resources not assigned to a specific shard. In most cases, this setting should simply be left alone.
   # shardName:
 
@@ -318,6 +325,9 @@ managementController:
   ## @param managementController.logLevel The log level for the management controller.
   logLevel: INFO
 
+  ## @param managementController.securityContext Security context for management controller pods.
+  securityContext: {}
+
   ## @param managementController.resources Resources limits and requests for the management controller containers.
   resources: {}
     # limits:
@@ -368,6 +378,8 @@ webhooksServer:
   affinity: {}
   ## @param webhooksServer.annotations Annotations to add to the webhooks server pods.
   annotations: {}
+  ## @param webhooksServer.securityContext Security context for webhooks server pods.
+  securityContext: {}
 
 ## @section Garbage Collector
 garbageCollector:
@@ -403,3 +415,5 @@ garbageCollector:
   affinity: {}
   ## @param garbageCollector.annotations Annotations to add to the garbage collector pods.
   annotations: {}
+  ## @param garbageCollector.securityContext Security context for garbage collector pods.
+  securityContext: {}
diff --git a/cmd/controlplane/api.go b/cmd/controlplane/api.go
index 905ddae6..a03ca388 100644
--- a/cmd/controlplane/api.go
+++ b/cmd/controlplane/api.go
@@ -22,17 +22,12 @@ import (
 	rollouts "github.com/akuity/kargo/internal/controller/rollouts/api/v1alpha1"
 	"github.com/akuity/kargo/internal/kubeclient"
 	"github.com/akuity/kargo/internal/os"
-	"github.com/akuity/kargo/internal/types"
 	versionpkg "github.com/akuity/kargo/internal/version"
 )
 
 type apiOptions struct {
 	KubeConfig string
 
-	RolloutsEnabled       bool
-	RolloutsKubeConfig    string
-	AnalysisRunsNamespace string
-
 	Host string
 	Port string
 
@@ -60,10 +55,6 @@ func newAPICommand() *cobra.Command {
 func (o *apiOptions) complete() {
 	o.KubeConfig = os.GetEnv("KUBECONFIG", "")
 
-	o.RolloutsEnabled = types.MustParseBool(os.GetEnv("ROLLOUTS_INTEGRATION_ENABLED", "true"))
-	o.RolloutsKubeConfig = os.GetEnv("ROLLOUTS_KUBECONFIG", "")
-	o.AnalysisRunsNamespace = os.GetEnv("ROLLOUTS_ANALYSIS_RUNS_NAMESPACE", "")
-
 	o.Host = os.GetEnv("HOST", "0.0.0.0")
 	o.Port = os.GetEnv("PORT", "8080")
 }
@@ -86,15 +77,15 @@ func (o *apiOptions) run(ctx context.Context) error {
 		return fmt.Errorf("error creating Kubernetes client for Kargo API server: %w", err)
 	}
 
-	var rolloutsAvailable bool
 	switch {
-	case !o.RolloutsEnabled:
+	case !cfg.RolloutsIntegrationEnabled:
 		o.Logger.Info("Argo Rollouts integration is disabled")
 	case !argoRolloutsExists(ctx, clientCfg):
 		o.Logger.Warn(
 			"Argo Rollouts integration was enabled, but no Argo Rollouts " +
 				"CRDs were found. Proceeding without Argo Rollouts integration.",
 		)
+		cfg.RolloutsIntegrationEnabled = false
 	default:
 		o.Logger.Info("Argo Rollouts integration is enabled")
 	}
@@ -110,7 +101,7 @@ func (o *apiOptions) run(ctx context.Context) error {
 		}).Info("SSO via OpenID Connect is enabled")
 	}
 
-	srv := api.NewServer(cfg, kubeClient, internalClient, rolloutsAvailable)
+	srv := api.NewServer(cfg, kubeClient, internalClient)
 	l, err := net.Listen("tcp", fmt.Sprintf("%s:%s", o.Host, o.Port))
 	if err != nil {
 		return fmt.Errorf("error creating listener: %w", err)
diff --git a/docs/docs/30-how-to-guides/15-working-with-freight.md b/docs/docs/30-how-to-guides/15-working-with-freight.md
index ce178b51..cf65ee89 100644
--- a/docs/docs/30-how-to-guides/15-working-with-freight.md
+++ b/docs/docs/30-how-to-guides/15-working-with-freight.md
@@ -137,7 +137,7 @@ field and label values, but this is expected to be a rare occurrence.
 
 ## Manual Approvals
 
-The [concepts doc](http://localhost:3000/concepts#verifications) describes the
+The [concepts doc](../concepts#verifications) describes the
 usual process by which `Freight` resources are _verified_ at each `Stage` in a
 pipeline before becoming available to the next `Stage` or `Stage`s. In brief, it
 typically requires the `Stage` to reach a healthy state _and_, if applicable,
diff --git a/internal/api/config/config.go b/internal/api/config/config.go
index 881e7203..220d16f9 100644
--- a/internal/api/config/config.go
+++ b/internal/api/config/config.go
@@ -27,6 +27,7 @@ type ServerConfig struct {
 	DexProxyConfig              *dex.ProxyConfig
 	ArgoCDConfig                ArgoCDConfig
 	PermissiveCORSPolicyEnabled bool
+	RolloutsIntegrationEnabled  bool
 }
 
 func ServerConfigFromEnv() ServerConfig {
@@ -51,6 +52,8 @@ func ServerConfigFromEnv() ServerConfig {
 	envconfig.MustProcess("", &cfg.ArgoCDConfig)
 	cfg.PermissiveCORSPolicyEnabled =
 		types.MustParseBool(os.GetEnv("PERMISSIVE_CORS_POLICY_ENABLED", "false"))
+	cfg.RolloutsIntegrationEnabled =
+		types.MustParseBool(os.GetEnv("ROLLOUTS_INTEGRATION_ENABLED", "true"))
 	return cfg
 }
 
diff --git a/internal/api/get_analysisrun_v1alpha1.go b/internal/api/get_analysisrun_v1alpha1.go
index fb8e2fab..710c305a 100644
--- a/internal/api/get_analysisrun_v1alpha1.go
+++ b/internal/api/get_analysisrun_v1alpha1.go
@@ -15,7 +15,7 @@ func (s *server) GetAnalysisRun(
 	ctx context.Context,
 	req *connect.Request[svcv1alpha1.GetAnalysisRunRequest],
 ) (*connect.Response[svcv1alpha1.GetAnalysisRunResponse], error) {
-	if s.getAnalysisRunFn == nil {
+	if !s.cfg.RolloutsIntegrationEnabled {
 		return nil, connect.NewError(
 			connect.CodeUnimplemented,
 			errors.New("Argo Rollouts integration is not enabled"),
diff --git a/internal/api/server.go b/internal/api/server.go
index e90c5962..95f01c25 100644
--- a/internal/api/server.go
+++ b/internal/api/server.go
@@ -144,7 +144,6 @@ func NewServer(
 	cfg config.ServerConfig,
 	kubeClient kubernetes.Client,
 	internalClient client.Client,
-	rolloutsEnabled bool,
 ) Server {
 	s := &server{
 		cfg:            cfg,
@@ -166,10 +165,7 @@ func NewServer(
 	s.patchFreightAliasFn = s.patchFreightAlias
 	s.patchFreightStatusFn = s.patchFreightStatus
 	s.authorizeFn = kubeClient.Authorize
-
-	if rolloutsEnabled {
-		s.getAnalysisRunFn = rollouts.GetAnalysisRun
-	}
+	s.getAnalysisRunFn = rollouts.GetAnalysisRun
 
 	return s
 }
diff --git a/internal/cli/cmd/server/server.go b/internal/cli/cmd/server/server.go
index 0a388edd..3c8c9259 100644
--- a/internal/cli/cmd/server/server.go
+++ b/internal/cli/cmd/server/server.go
@@ -99,7 +99,6 @@ func (o *serverOptions) run(ctx context.Context) error {
 		},
 		client,
 		client,
-		true,
 	)
 	if err := srv.Serve(ctx, l); err != nil {
 		return fmt.Errorf("serve error: %w", err)
diff --git a/ui/src/features/project/list/create-project-modal.tsx b/ui/src/features/project/list/create-project-modal.tsx
index 2a7008b5..ee25f44c 100644
--- a/ui/src/features/project/list/create-project-modal.tsx
+++ b/ui/src/features/project/list/create-project-modal.tsx
@@ -1,5 +1,5 @@
-import { zodResolver } from '@hookform/resolvers/zod';
 import { createConnectQueryKey, useMutation } from '@connectrpc/connect-query';
+import { zodResolver } from '@hookform/resolvers/zod';
 import { useQueryClient } from '@tanstack/react-query';
 import { Form, Input, Modal, Tabs } from 'antd';
 import type { JSONSchema4 } from 'json-schema';
@@ -44,7 +44,8 @@ export const CreateProjectModal = ({ visible, hide }: ModalComponentProps) => {
         manifest: textEncoder.encode(data.value)
       },
       {
-        onSuccess: () => queryClient.invalidateQueries({ queryKey: createConnectQueryKey(listProjects) })
+        onSuccess: () =>
+          queryClient.invalidateQueries({ queryKey: createConnectQueryKey(listProjects) })
       }
     );
   });
diff --git a/ui/src/features/project/project-details/project-details.tsx b/ui/src/features/project/project-details/project-details.tsx
index cd390733..077e1220 100644
--- a/ui/src/features/project/project-details/project-details.tsx
+++ b/ui/src/features/project/project-details/project-details.tsx
@@ -57,7 +57,7 @@ import { Images } from './images';
 import { RepoNode } from './nodes/repo-node';
 import { Nodule, StageNode } from './nodes/stage-node';
 import styles from './project-details.module.less';
-import { NodeType, NodesItemType, NodesRepoType } from './types';
+import { NewWarehouseNode, NodeType, NodesItemType, NodesRepoType } from './types';
 import { UpdateFreightAliasModal } from './update-freight-alias-modal';
 
 const lineThickness = 2;
@@ -224,6 +224,10 @@ export const ProjectDetails = () => {
 
     const warehouseNodeMap = {} as { [key: string]: NodesRepoType };
 
+    (warehouseData?.warehouses || []).map((warehouse) => {
+      warehouseNodeMap[warehouse.metadata?.name || ''] = NewWarehouseNode(warehouse);
+    });
+
     const myNodes = data.stages
       .slice()
       .sort((a, b) => a.metadata?.name?.localeCompare(b.metadata?.name || '') || 0)
@@ -240,13 +244,7 @@ export const ProjectDetails = () => {
         if (warehouseName) {
           const cur = warehouseMap[warehouseName];
           if (!warehouseNodeMap[warehouseName] && cur) {
-            warehouseNodeMap[warehouseName] = {
-              data: cur?.metadata?.name || '',
-              stageNames: [stage.metadata?.name || ''],
-              warehouseName: cur?.metadata?.name || '',
-              refreshing: !!cur?.metadata?.annotations['kargo.akuity.io/refresh'],
-              type: NodeType.WAREHOUSE
-            };
+            warehouseNodeMap[warehouseName] = NewWarehouseNode(cur, [stage.metadata?.name || '']);
           } else {
             const stageNames = [
               ...(warehouseNodeMap[warehouseName]?.stageNames || []),
@@ -528,8 +526,14 @@ export const ProjectDetails = () => {
 
   if (isLoading || isLoadingFreight) return <LoadingState />;
 
-  if (!data || data.stages.length === 0) return <Empty />;
-  const stage = stageName && data.stages.find((item) => item.metadata?.name === stageName);
+  if (
+    (!data || data.stages.length === 0) &&
+    (!warehouseData || warehouseData.warehouses.length === 0)
+  ) {
+    return <Empty />;
+  }
+
+  const stage = stageName && (data?.stages || []).find((item) => item.metadata?.name === stageName);
 
   const isFaded = (stage: Stage): boolean => {
     if (!promotingStage || !confirmingPromotion) {
diff --git a/ui/src/features/project/project-details/types.ts b/ui/src/features/project/project-details/types.ts
index fbe63f5b..970c1bdd 100644
--- a/ui/src/features/project/project-details/types.ts
+++ b/ui/src/features/project/project-details/types.ts
@@ -2,7 +2,8 @@ import {
   ChartSubscription,
   GitSubscription,
   ImageSubscription,
-  Stage
+  Stage,
+  Warehouse
 } from '@ui/gen/v1alpha1/generated_pb';
 
 export enum NodeType {
@@ -46,3 +47,14 @@ export type NodesItemType =
       color: string;
     }
   | NodesRepoType;
+
+export const NewWarehouseNode = (warehouse: Warehouse, stageNames?: string[]): NodesRepoType => {
+  const name = warehouse?.metadata?.name || '';
+  return {
+    data: name,
+    stageNames: stageNames || [],
+    warehouseName: name,
+    refreshing: !!warehouse?.metadata?.annotations['kargo.akuity.io/refresh'],
+    type: NodeType.WAREHOUSE
+  };
+};


Test patch:
diff --git a/internal/api/get_analysisrun_v1alpha1_test.go b/internal/api/get_analysisrun_v1alpha1_test.go
index fbea4c39..520beace 100644
--- a/internal/api/get_analysisrun_v1alpha1_test.go
+++ b/internal/api/get_analysisrun_v1alpha1_test.go
@@ -14,6 +14,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 
+	"github.com/akuity/kargo/internal/api/config"
 	"github.com/akuity/kargo/internal/api/kubernetes"
 	"github.com/akuity/kargo/internal/api/user"
 	rollouts "github.com/akuity/kargo/internal/controller/rollouts/api/v1alpha1"
@@ -23,6 +24,7 @@ import (
 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)
 		errExpected      bool
 		expectedCode     connect.Code
@@ -86,7 +88,7 @@ func TestGetAnalysisRun(t *testing.T) {
 				Namespace: "kargo-demo",
 				Name:      "test",
 			},
-			getAnalysisRunFn: nil,
+			rolloutsDisabled: true,
 			errExpected:      true,
 			expectedCode:     connect.CodeUnimplemented,
 		},
@@ -105,6 +107,11 @@ func TestGetAnalysisRun(t *testing.T) {
 				},
 			)
 
+			cfg := config.ServerConfigFromEnv()
+			if testCase.rolloutsDisabled {
+				cfg.RolloutsIntegrationEnabled = false
+			}
+
 			c, err := kubernetes.NewClient(
 				ctx,
 				&rest.Config{},
@@ -131,6 +138,7 @@ func TestGetAnalysisRun(t *testing.T) {
 			require.NoError(t, err)
 
 			svr := &server{
+				cfg:              cfg,
 				client:           c,
 				getAnalysisRunFn: testCase.getAnalysisRunFn,
 			}
diff --git a/internal/api/server_test.go b/internal/api/server_test.go
index 6c737d57..e7fad298 100644
--- a/internal/api/server_test.go
+++ b/internal/api/server_test.go
@@ -31,46 +31,29 @@ func TestNewServer(t *testing.T) {
 	)
 	require.NoError(t, err)
 
-	t.Run("Default", func(t *testing.T) {
-		s, ok := NewServer(
-			testServerConfig,
-			testClient,
-			fake.NewClientBuilder().Build(),
-			false,
-		).(*server)
-
-		require.True(t, ok)
-		require.NotNil(t, s)
-		require.Same(t, testClient, s.client)
-		require.Equal(t, testServerConfig, s.cfg)
-		require.NotNil(t, s.validateProjectExistsFn)
-		require.NotNil(t, s.externalValidateProjectFn)
-		require.NotNil(t, s.getStageFn)
-		require.NotNil(t, s.getFreightByNameOrAliasFn)
-		require.NotNil(t, s.isFreightAvailableFn)
-		require.NotNil(t, s.createPromotionFn)
-		require.NotNil(t, s.findStageSubscribersFn)
-		require.NotNil(t, s.listFreightFn)
-		require.NotNil(t, s.getAvailableFreightForStageFn)
-		require.NotNil(t, s.getFreightFromWarehouseFn)
-		require.NotNil(t, s.getVerifiedFreightFn)
-		require.NotNil(t, s.patchFreightAliasFn)
-		require.NotNil(t, s.patchFreightStatusFn)
-		require.NotNil(t, s.authorizeFn)
-		require.Nil(t, s.getAnalysisRunFn)
-	})
-
-	t.Run("RolloutsEnabled", func(t *testing.T) {
-		s, ok := NewServer(
-			testServerConfig,
-			testClient,
-			fake.NewClientBuilder().Build(),
-			true,
-		).(*server)
-
-		require.True(t, ok)
-		require.NotNil(t, s)
-
-		require.NotNil(t, s.getAnalysisRunFn)
-	})
+	s, ok := NewServer(
+		testServerConfig,
+		testClient,
+		fake.NewClientBuilder().Build(),
+	).(*server)
+
+	require.True(t, ok)
+	require.NotNil(t, s)
+	require.Same(t, testClient, s.client)
+	require.Equal(t, testServerConfig, s.cfg)
+	require.NotNil(t, s.validateProjectExistsFn)
+	require.NotNil(t, s.externalValidateProjectFn)
+	require.NotNil(t, s.getStageFn)
+	require.NotNil(t, s.getFreightByNameOrAliasFn)
+	require.NotNil(t, s.isFreightAvailableFn)
+	require.NotNil(t, s.createPromotionFn)
+	require.NotNil(t, s.findStageSubscribersFn)
+	require.NotNil(t, s.listFreightFn)
+	require.NotNil(t, s.getAvailableFreightForStageFn)
+	require.NotNil(t, s.getFreightFromWarehouseFn)
+	require.NotNil(t, s.getVerifiedFreightFn)
+	require.NotNil(t, s.patchFreightAliasFn)
+	require.NotNil(t, s.patchFreightStatusFn)
+	require.NotNil(t, s.authorizeFn)
+	require.NotNil(t, s.getAnalysisRunFn)
 }