Repository: akuity/kargo
Pull request #1874 (2024-04-19 14:19:42)

feat(chart): Add optional env and envFrom parameters for all deployments and cronjobs
Adds the possibility to add env values or envFrom values for all deployments and cronjobs. We use kargo on a cluster behind a webproxy and needed a way to configure http_proxy and no_proxy variables in the kargo-controller. Currently we work around this by patching the deployment with kustomize. With this PR it can be done via helm-values.

While I technically only need these values on the kargo-controller, I wanted to add the env and envFrom values to all deployments to keep it consistent.

I tried to stay close to how argocd lets you configure these values in their helm charts. https://github.com/argoproj/argo-helm/tree/main/charts/argo-cd

Used this values file locally to check that the functionality works:
```yaml
global:
  env:
    - name: GLOBAL_ENV
      value: "========="

api:
  env:
    - name: EXTRA_ENV_API
      value: "-----------------"
  envFrom:
    - configMapRef:
        name: EXTRA_CM_API
  oidc:
    enabled: true
    dex:
      enabled: true
      env:
        - name: EXTRA_ENV_DEX
          value: "-----------------"
      envFrom:
        - configMapRef:
            name: EXTRA_CM_DEX
  adminAccount:
    passwordHash: $2a$12$qG6iGJgTpQbZo3dRi3q1a.gYg8Na8PF8f263VcdcQNN.squppsuSG
    tokenSigningKey: iwishtowashmyirishwristwatch

controller:
  env:
    - name: EXTRA_ENV_CONTROLLER
      value: "-----------------"
  envFrom:
    - configMapRef:
        name: EXTRA_CM_CONTROLLER

garbageCollector:
  env:
    - name: EXTRA_ENV_GARBAGE_COLLECTOR
      value: "-----------------"
  envFrom:
    - configMapRef:
        name: EXTRA_ENV_GARBAGE_COLLECTOR

managementController:
  env:
    - name: EXTRA_ENV_MANAGEMENT_CONTROLLER
      value: "-----------------"
  envFrom:
    - configMapRef:
        name: EXTRA_ENV_MANAGEMENT_CONTROLLER

webhooksServer:
  env:
    - name: EXTRA_ENV_WEBHOOK
      value: "-----------------"
  envFrom:
    - configMapRef:
        name: EXTRA_CM_WEBHOOK
```

With this command:
```yaml
helm template kargo . --namespace kargo --create-namespace  -f test.values.yaml > test.yaml
```

Best regards. Thanks for your work.

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


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | b5246cae2fa01ff20d3d722d30b83cd3d3f22963 |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/docs-kargo-akuity-io/deploys/66227d81e825450008a6c917 |
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/akuity/kargo/pull/1874?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 46.42%. Comparing base [(`83a3b3c`)](https://app.codecov.io/gh/akuity/kargo/commit/83a3b3caba92690f80a21dd5bcb7e7512aa188d8?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=akuity) to head [(`b5246ca`)](https://app.codecov.io/gh/akuity/kargo/pull/1874?dropdown=coverage&src=pr&el=desc&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    #1874   +/-   ##
=======================================
  Coverage   46.42%   46.42%           
=======================================
  Files         215      215           
  Lines       14039    14039           
=======================================
  Hits         6517     6517           
  Misses       7228     7228           
  Partials      294      294           
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/akuity/kargo/pull/1874?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/api/v1alpha1/annotations.go b/api/v1alpha1/annotations.go
index 45c17be0..21cc55c5 100644
--- a/api/v1alpha1/annotations.go
+++ b/api/v1alpha1/annotations.go
@@ -3,6 +3,10 @@ package v1alpha1
 import "encoding/json"
 
 const (
+	// AnnotationKeyCreateActor is an annotation key that can be injected to a resource
+	// by the Kargo controlplane to indicate the actor that created the resource.
+	AnnotationKeyCreateActor = "kargo.akuity.io/create-actor"
+
 	// AnnotationKeyRefresh is an annotation key that can be set on a resource
 	// to trigger a refresh of the resource by the controller. The value of the
 	// annotation is interpreted as a token, and any change to the value of the
diff --git a/api/v1alpha1/event.go b/api/v1alpha1/event.go
index 5405bfc2..6b4ac4bd 100644
--- a/api/v1alpha1/event.go
+++ b/api/v1alpha1/event.go
@@ -93,9 +93,16 @@ func NewPromotionEventAnnotations(
 		AnnotationKeyEventStageName:           p.Spec.Stage,
 		AnnotationKeyEventPromotionCreateTime: p.GetCreationTimestamp().Format(time.RFC3339),
 	}
+
 	if actor != "" {
 		annotations[AnnotationKeyEventActor] = actor
 	}
+	// All Promotion-related events are emitted after the promotion was created.
+	// Therefore, if the promotion knows who triggered it, set them as an actor.
+	if promoteActor, ok := p.Annotations[AnnotationKeyCreateActor]; ok {
+		annotations[AnnotationKeyEventActor] = promoteActor
+	}
+
 	if f != nil {
 		annotations[AnnotationKeyEventFreightCreateTime] = f.CreationTimestamp.Format(time.RFC3339)
 		annotations[AnnotationKeyEventFreightAlias] = f.Alias
diff --git a/charts/kargo/README.md b/charts/kargo/README.md
index d9517aad..f123da52 100644
--- a/charts/kargo/README.md
+++ b/charts/kargo/README.md
@@ -3,6 +3,13 @@
 
 ## Parameters
 
+### Global Parameters
+
+| Name             | Description                                                                | Value |
+| ---------------- | -------------------------------------------------------------------------- | ----- |
+| `global.env`     | Environment variables to add to all Kargo pods.                            | `[]`  |
+| `global.envFrom` | Environment variables to add to all Kargo pods from ConfigMaps or Secrets. | `[]`  |
+
 ### Image Parameters
 
 | Name                | Description                                                                  | Value                  |
@@ -70,6 +77,8 @@ the Kargo controller is running.
 | `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.env`                                   | Environment variables to add to API server pods.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | `[]`                     |
+| `api.envFrom`                               | Environment variables to add to API server pods from ConfigMaps or Secrets.                                                                                                                                                                                                                                                                                                                                                                                                                                                     | `[]`                     |
 | `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`                   |
@@ -82,6 +91,7 @@ the Kargo controller is running.
 | `api.ingress.pathType`                      | You may want to use `Prefix` for some controllers (like AWS LoadBalancer Ingress controller), which don't support `/` as wildcard path when pathType is set to `ImplementationSpecific`                                                                                                                                                                                                                                                                                                                                         | `ImplementationSpecific` |
 | `api.service.type`                          | If you're not going to use an ingress controller, you may want to change this value to `LoadBalancer` for production deployments. If running locally, you may want to change it to `NodePort` OR leave it as `ClusterIP` and use `kubectl port-forward` to map a port on the local network interface to the service.                                                                                                                                                                                                            | `ClusterIP`              |
 | `api.service.nodePort`                      | Host port the `Service` will be mapped to when `type` is either `NodePort` or `LoadBalancer`. If not specified, Kubernetes chooses.                                                                                                                                                                                                                                                                                                                                                                                             | `undefined`              |
+| `api.service.annotations`                   | Annotations to add to the API server's service.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | `{}`                     |
 | `api.secret.name`                           | Specifies the name of an existing Secret which contains the `ADMIN_ACCOUNT_PASSWORD_HASH` and `ADMIN_ACCOUNT_TOKEN_SIGNING_KEY` values. By setting this, the Secret will **not** be generated by Helm.                                                                                                                                                                                                                                                                                                                          | `""`                     |
 | `api.adminAccount.enabled`                  | Whether to enable the admin account.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | `true`                   |
 | `api.adminAccount.passwordHash`             | Bcrypt password hash for the admin account. A value **must** be provided for this field unless `api.secret.name` is specified.                                                                                                                                                                                                                                                                                                                                                                                                  | `""`                     |
@@ -103,7 +113,8 @@ the Kargo controller is running.
 | `api.oidc.dex.image.tag`                    | Image tag for Dex.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | `v2.37.0`                |
 | `api.oidc.dex.image.pullPolicy`             | Image pull policy for Dex.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `IfNotPresent`           |
 | `api.oidc.dex.image.pullSecrets`            | List of imagePullSecrets.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | `[]`                     |
-| `api.oidc.dex.env`                          | Add additional environment variables to Dex pods. This is convenient for cases where api.oidc.dex.connectors needs to reference environment variables from a Secret that is managed "out of band" with a secret management solution such as Sealed Secrets.                                                                                                                                                                                                                                                                     | `[]`                     |
+| `api.oidc.dex.env`                          | Environment variables to add to Dex server pods. This is convenient for cases where api.oidc.dex.connectors needs to reference environment variables from a Secret that is managed "out of band" with a secret management solution such as Sealed Secrets.                                                                                                                                                                                                                                                                      | `[]`                     |
+| `api.oidc.dex.envFrom`                      | Environment variables to add to Dex server pods from ConfigMaps or Secrets. This is especially convenient for cases where api.oidc.dex.connectors needs to reference environment variables from a Secret that is managed "out of band" with a secret management solution such as Sealed Secrets.                                                                                                                                                                                                                                | `[]`                     |
 | `api.oidc.dex.volumes`                      | Add additional volumes to Dex pods. This is convenient for cases where api.oidc.dex.connectors needs to reference mounted data from a Secret that is managed "out of band" with a secret management solution such as Sealed Secrets.                                                                                                                                                                                                                                                                                            | `[]`                     |
 | `api.oidc.dex.volumeMounts`                 | Add additional volume mounts to Dex pods. This is convenient for cases where api.oidc.dex.connectors needs to reference mounted data from a Secret that is managed "out of band" with a secret management solution such as Sealed Secrets.                                                                                                                                                                                                                                                                                      | `nil`                    |
 | `api.oidc.dex.probes.enabled`               | Whether liveness and readiness probes should be included in the Dex server deployment. It is sometimes advantageous to disable these during local development.                                                                                                                                                                                                                                                                                                                                                                  | `true`                   |
@@ -142,19 +153,23 @@ the Kargo controller is running.
 | `controller.tolerations`                     | Tolerations for controller pods.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | `[]`                     |
 | `controller.affinity`                        | Specifies pod affinity for controller pods.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `{}`                     |
 | `controller.annotations`                     | Annotations to add to the controller pods.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | `{}`                     |
+| `controller.env`                             | Environment variables to add to controller pods.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | `[]`                     |
+| `controller.envFrom`                         | Environment variables to add to controller pods from ConfigMaps or Secrets.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | `[]`                     |
 
 ### 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.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.                   | `{}`   |
+| 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.                                  | `{}`   |
+| `managementController.env`             | Environment variables to add to management controller pods.                            | `[]`   |
+| `managementController.envFrom`         | Environment variables to add to management controller pods from ConfigMaps or Secrets. | `[]`   |
 
 ### Webhooks
 
@@ -177,6 +192,8 @@ the Kargo controller is running.
 | `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.                                                                                                                                                                                                                                                                                                                                            | `{}`   |
+| `webhooksServer.env`                   | Environment variables to add to webhook server pods.                                                                                                                                                                                                                                                                                                                                  | `[]`   |
+| `webhooksServer.envFrom`               | Environment variables to add to webhook server pods from ConfigMaps or Secrets.                                                                                                                                                                                                                                                                                                       | `[]`   |
 
 ### Garbage Collector
 
@@ -196,4 +213,6 @@ the Kargo controller is running.
 | `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.                                                                                                                                                                                                                                                                              | `{}`        |
+| `garbageCollector.env`                     | Environment variables to add to garbage collector pods.                                                                                                                                                                                                                                                                   | `[]`        |
+| `garbageCollector.envFrom`                 | Environment variables to add to garbage collector pods from ConfigMaps or Secrets.                                                                                                                                                                                                                                        | `[]`        |
 
diff --git a/charts/kargo/templates/api/deployment.yaml b/charts/kargo/templates/api/deployment.yaml
index 1a65a414..209be99b 100644
--- a/charts/kargo/templates/api/deployment.yaml
+++ b/charts/kargo/templates/api/deployment.yaml
@@ -42,11 +42,18 @@ spec:
           image: {{ include "kargo.image" . }}
           imagePullPolicy: {{ .Values.image.pullPolicy }}
           command: ["/usr/local/bin/kargo", "api"]
+          {{- with (concat .Values.global.env .Values.api.env) }}
+          env:
+            {{- toYaml . | nindent 10 }}
+          {{- end }}
           envFrom:
-            - configMapRef:
-                name: kargo-api
-            - secretRef:
-                name: {{ .Values.api.secret.name | default "kargo-api" }}
+          - configMapRef:
+              name: kargo-api
+          - secretRef:
+              name: {{ .Values.api.secret.name | default "kargo-api" }}
+          {{- with (concat .Values.global.envFrom .Values.api.envFrom) }}
+            {{- toYaml . | nindent 10 }}
+          {{- end }}
           ports:
             - name: h2c
               containerPort: 8080
diff --git a/charts/kargo/templates/api/service.yaml b/charts/kargo/templates/api/service.yaml
index 6a0dda13..c026f233 100644
--- a/charts/kargo/templates/api/service.yaml
+++ b/charts/kargo/templates/api/service.yaml
@@ -7,6 +7,10 @@ metadata:
   labels:
     {{- include "kargo.labels" . | nindent 4 }}
     {{- include "kargo.api.labels" . | nindent 4 }}
+  {{- with .Values.api.service.annotations }}
+  annotations:
+    {{- toYaml . | nindent 4 }}
+  {{- end }}
 spec:
   type: {{ .Values.api.service.type }}
   ports:
diff --git a/charts/kargo/templates/controller/deployment.yaml b/charts/kargo/templates/controller/deployment.yaml
index f90445eb..1fd866bb 100644
--- a/charts/kargo/templates/controller/deployment.yaml
+++ b/charts/kargo/templates/controller/deployment.yaml
@@ -41,9 +41,16 @@ spec:
         image: {{ include "kargo.image" . }}
         imagePullPolicy: {{ .Values.image.pullPolicy }}
         command: ["/usr/local/bin/kargo", "controller"]
+        {{- with (concat .Values.global.env .Values.controller.env) }}
+        env:
+          {{- toYaml . | nindent 8 }}
+        {{- end }}
         envFrom:
         - configMapRef:
             name: kargo-controller
+        {{- with (concat .Values.global.envFrom .Values.controller.envFrom) }}
+          {{- toYaml . | nindent 8 }}
+        {{- end }}
         {{- if or .Values.kubeconfigSecrets.kargo .Values.kubeconfigSecrets.argocd .Values.controller.gitClient.signingKeySecret.name }}
         volumeMounts:
         {{- if or .Values.kubeconfigSecrets.kargo .Values.kubeconfigSecrets.argocd }}
diff --git a/charts/kargo/templates/dex-server/deployment.yaml b/charts/kargo/templates/dex-server/deployment.yaml
index 466bda61..1150eb25 100644
--- a/charts/kargo/templates/dex-server/deployment.yaml
+++ b/charts/kargo/templates/dex-server/deployment.yaml
@@ -40,8 +40,12 @@ spec:
         imagePullPolicy: {{ .Values.api.oidc.dex.image.pullPolicy }}
         command: ["dex", "serve"]
         args: ["/etc/dex/config.yaml"]
+        {{- with (concat .Values.global.env .Values.api.oidc.dex.env) }}
         env:
-        {{- with .Values.api.oidc.dex.env }}
+          {{- toYaml . | nindent 8 }}
+        {{- end }}
+        {{- with (concat .Values.global.envFrom .Values.api.oidc.dex.envFrom) }}
+        envFrom:
           {{- toYaml . | nindent 8 }}
         {{- end }}
         volumeMounts:
diff --git a/charts/kargo/templates/garbage-collector/cron-job.yaml b/charts/kargo/templates/garbage-collector/cron-job.yaml
index fc65b025..620ee93c 100644
--- a/charts/kargo/templates/garbage-collector/cron-job.yaml
+++ b/charts/kargo/templates/garbage-collector/cron-job.yaml
@@ -34,9 +34,16 @@ spec:
             image: {{ include "kargo.image" . }}
             imagePullPolicy: {{ .Values.image.pullPolicy }}
             command: ["/usr/local/bin/kargo", "garbage-collector"]
+            {{- with (concat .Values.global.env .Values.garbageCollector.env) }}
+            env:
+              {{- toYaml . | nindent 12 }}
+            {{- end }}
             envFrom:
             - configMapRef:
                 name: kargo-garbage-collector
+            {{- with (concat .Values.global.envFrom .Values.garbageCollector.envFrom) }}
+              {{- toYaml . | nindent 12 }}
+            {{- end }}
             securityContext:
               {{- toYaml .Values.garbageCollector.securityContext | nindent 14 }}
             resources:
diff --git a/charts/kargo/templates/management-controller/deployment.yaml b/charts/kargo/templates/management-controller/deployment.yaml
index 1fd82896..c1a85997 100644
--- a/charts/kargo/templates/management-controller/deployment.yaml
+++ b/charts/kargo/templates/management-controller/deployment.yaml
@@ -41,9 +41,16 @@ spec:
         image: {{ include "kargo.image" . }}
         imagePullPolicy: {{ .Values.image.pullPolicy }}
         command: ["/usr/local/bin/kargo", "management-controller"]
+        {{- with (concat .Values.global.env .Values.managementController.env) }}
+        env:
+          {{- toYaml . | nindent 8 }}
+        {{- end }}
         envFrom:
         - configMapRef:
             name: kargo-management-controller
+        {{- with (concat .Values.global.envFrom .Values.managementController.envFrom) }}
+          {{- toYaml . | nindent 8 }}
+        {{- end }}
         {{- if .Values.kubeconfigSecrets.kargo }}
         volumeMounts:
         - mountPath: /etc/kargo/kubeconfigs
diff --git a/charts/kargo/templates/webhooks-server/deployment.yaml b/charts/kargo/templates/webhooks-server/deployment.yaml
index 0607173f..5d948b1d 100644
--- a/charts/kargo/templates/webhooks-server/deployment.yaml
+++ b/charts/kargo/templates/webhooks-server/deployment.yaml
@@ -41,9 +41,16 @@ spec:
         image: {{ include "kargo.image" . }}
         imagePullPolicy: {{ .Values.image.pullPolicy }}
         command: ["/usr/local/bin/kargo", "webhooks-server"]
+        {{- with (concat .Values.global.env .Values.webhooksServer.env) }}
+        env:
+          {{- toYaml . | nindent 8 }}
+        {{- end }}
         envFrom:
         - configMapRef:
             name: kargo-webhooks-server
+        {{- with (concat .Values.global.envFrom .Values.webhooksServer.envFrom) }}
+          {{- toYaml . | nindent 8 }}
+        {{- end }}
         ports:
         - containerPort: 9443
           name: webhooks-server
diff --git a/charts/kargo/values.yaml b/charts/kargo/values.yaml
index 8eed4bff..e86436f5 100755
--- a/charts/kargo/values.yaml
+++ b/charts/kargo/values.yaml
@@ -3,6 +3,19 @@
 ## This is a YAML-formatted file.
 ## Declare variables to be passed into your templates.
 
+## @section Global Parameters
+global:
+  ## @param global.env Environment variables to add to all Kargo pods.
+  env: []
+  #  - name: ENV_NAME
+  #    value: value
+  ## @param global.envFrom Environment variables to add to all Kargo pods from ConfigMaps or Secrets.
+  envFrom: []
+  #  - configMapRef:
+  #      name: config-map-name
+  #  - secretRef:
+  #      name: secret-name
+
 ## @section Image Parameters
 image:
   ## @param image.repository Image repository of Kargo
@@ -88,6 +101,16 @@ api:
   annotations: {}
   ## @param api.securityContext Security context for api pods.
   securityContext: {}
+  ## @param api.env Environment variables to add to API server pods.
+  env: []
+  #  - name: ENV_NAME
+  #    value: value
+  ## @param api.envFrom Environment variables to add to API server pods from ConfigMaps or Secrets.
+  envFrom: []
+  #  - configMapRef:
+  #      name: config-map-name
+  #  - secretRef:
+  #      name: secret-name
 
   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.
@@ -123,6 +146,8 @@ api:
     type: ClusterIP
     ## @param api.service.nodePort [nullable] Host port the `Service` will be mapped to when `type` is either `NodePort` or `LoadBalancer`. If not specified, Kubernetes chooses.
     # nodePort:
+    ## @param api.service.annotations Annotations to add to the API server's service. 
+    annotations: {}
 
   secret:
     ## @param api.secret.name Specifies the name of an existing Secret which contains the `ADMIN_ACCOUNT_PASSWORD_HASH` and `ADMIN_ACCOUNT_TOKEN_SIGNING_KEY` values. By setting this, the Secret will **not** be generated by Helm.
@@ -188,13 +213,19 @@ api:
         pullSecrets: []
         # - name: regcred
 
-      ## @param api.oidc.dex.env Add additional environment variables to Dex pods. This is convenient for cases where api.oidc.dex.connectors needs to reference environment variables from a Secret that is managed "out of band" with a secret management solution such as Sealed Secrets.
+      ## @param api.oidc.dex.env Environment variables to add to Dex server pods. This is convenient for cases where api.oidc.dex.connectors needs to reference environment variables from a Secret that is managed "out of band" with a secret management solution such as Sealed Secrets.
       env: []
       # - name: CLIENT_SECRET
       #   valueFrom:
       #     secretKeyRef:
       #       name: github-dex
       #       key: dex.github.clientSecret
+      ## @param api.oidc.dex.envFrom Environment variables to add to Dex server pods from ConfigMaps or Secrets. This is especially convenient for cases where api.oidc.dex.connectors needs to reference environment variables from a Secret that is managed "out of band" with a secret management solution such as Sealed Secrets.
+      envFrom: []
+      #  - configMapRef:
+      #      name: config-map-name
+      #  - secretRef:
+      #      name: secret-name
 
       ## @param api.oidc.dex.volumes Add additional volumes to Dex pods. This is convenient for cases where api.oidc.dex.connectors needs to reference mounted data from a Secret that is managed "out of band" with a secret management solution such as Sealed Secrets.
       volumes: []
@@ -336,6 +367,16 @@ controller:
   affinity: {}
   ## @param controller.annotations Annotations to add to the controller pods.
   annotations: {}
+  ## @param controller.env Environment variables to add to controller pods.
+  env: []
+  #  - name: ENV_NAME
+  #    value: value
+  ## @param controller.envFrom Environment variables to add to controller pods from ConfigMaps or Secrets.
+  envFrom: []
+  #  - configMapRef:
+  #      name: config-map-name
+  #  - secretRef:
+  #      name: secret-name
 
 ## @section Management Controller
 ## All settings for the management controller component
@@ -365,6 +406,16 @@ managementController:
   affinity: {}
   ## @param managementController.annotations Annotations to add to the management controller pods.
   annotations: {}
+  ## @param managementController.env Environment variables to add to management controller pods.
+  env: []
+  #  - name: ENV_NAME
+  #    value: value
+  ## @param managementController.envFrom Environment variables to add to management controller pods from ConfigMaps or Secrets.
+  envFrom: []
+  #  - configMapRef:
+  #      name: config-map-name
+  #  - secretRef:
+  #      name: secret-name
 
 ## @section Webhooks
 webhooks:
@@ -403,6 +454,16 @@ webhooksServer:
   annotations: {}
   ## @param webhooksServer.securityContext Security context for webhooks server pods.
   securityContext: {}
+  ## @param webhooksServer.env Environment variables to add to webhook server pods.
+  env: []
+  #  - name: ENV_NAME
+  #    value: value
+  ## @param webhooksServer.envFrom Environment variables to add to webhook server pods from ConfigMaps or Secrets.
+  envFrom: []
+  #  - configMapRef:
+  #      name: config-map-name
+  #  - secretRef:
+  #      name: secret-name
 
 ## @section Garbage Collector
 garbageCollector:
@@ -440,3 +501,13 @@ garbageCollector:
   annotations: {}
   ## @param garbageCollector.securityContext Security context for garbage collector pods.
   securityContext: {}
+  ## @param garbageCollector.env Environment variables to add to garbage collector pods.
+  env: []
+  #  - name: ENV_NAME
+  #    value: value
+  ## @param garbageCollector.envFrom Environment variables to add to garbage collector pods from ConfigMaps or Secrets.
+  envFrom: []
+  #  - configMapRef:
+  #      name: config-map-name
+  #  - secretRef:
+  #      name: secret-name
diff --git a/internal/api/promote_to_stage_subscribers_v1alpha1.go b/internal/api/promote_to_stage_subscribers_v1alpha1.go
index 3b84143d..08ef153a 100644
--- a/internal/api/promote_to_stage_subscribers_v1alpha1.go
+++ b/internal/api/promote_to_stage_subscribers_v1alpha1.go
@@ -133,7 +133,7 @@ func (s *server) PromoteToStageSubscribers(
 	promoteErrs := make([]error, 0, len(subscribers))
 	createdPromos := make([]*kargoapi.Promotion, 0, len(subscribers))
 	for _, subscriber := range subscribers {
-		newPromo := kargo.NewPromotion(subscriber, freight.Name)
+		newPromo := kargo.NewPromotion(ctx, subscriber, freight.Name)
 		if subscriber.Spec.PromotionMechanisms == nil {
 			// Avoid creating a Promotion if the subscriber has no
 			// PromotionMechanisms, and is a "control flow" Stage.
diff --git a/internal/api/promote_to_stage_v1alpha1.go b/internal/api/promote_to_stage_v1alpha1.go
index 5b5580d0..e22e9960 100644
--- a/internal/api/promote_to_stage_v1alpha1.go
+++ b/internal/api/promote_to_stage_v1alpha1.go
@@ -118,7 +118,7 @@ func (s *server) PromoteToStage(
 		return nil, err
 	}
 
-	promotion := kargo.NewPromotion(*stage, freight.Name)
+	promotion := kargo.NewPromotion(ctx, *stage, freight.Name)
 	if err := s.createPromotionFn(ctx, &promotion); err != nil {
 		return nil, fmt.Errorf("create promotion: %w", err)
 	}
diff --git a/internal/controller/stages/stages.go b/internal/controller/stages/stages.go
index 2e7f4e06..a63ab8ef 100644
--- a/internal/controller/stages/stages.go
+++ b/internal/controller/stages/stages.go
@@ -989,7 +989,7 @@ func (r *reconciler) syncNormalStage(
 
 	logger.Debug("auto-promotion will proceed")
 
-	promo := kargo.NewPromotion(*stage, latestFreight.Name)
+	promo := kargo.NewPromotion(ctx, *stage, latestFreight.Name)
 	if err :=
 		r.createPromotionFn(ctx, &promo); err != nil {
 		return status, fmt.Errorf(
diff --git a/internal/kargo/kargo.go b/internal/kargo/kargo.go
index 68c32dd1..4824a061 100644
--- a/internal/kargo/kargo.go
+++ b/internal/kargo/kargo.go
@@ -1,6 +1,7 @@
 package kargo
 
 import (
+	"context"
 	"fmt"
 	"strings"
 
@@ -11,6 +12,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/predicate"
 
 	kargoapi "github.com/akuity/kargo/api/v1alpha1"
+	"github.com/akuity/kargo/internal/api/user"
 )
 
 const (
@@ -22,7 +24,11 @@ const (
 
 // NewPromotion returns a new Promotion from a given stage and freight with our
 // naming convention.
-func NewPromotion(stage kargoapi.Stage, freight string) kargoapi.Promotion {
+func NewPromotion(
+	ctx context.Context,
+	stage kargoapi.Stage,
+	freight string,
+) kargoapi.Promotion {
 	shortHash := freight
 	if len(shortHash) > 7 {
 		shortHash = freight[0:7]
@@ -32,14 +38,21 @@ func NewPromotion(stage kargoapi.Stage, freight string) kargoapi.Promotion {
 		shortStageName = shortStageName[0:maxStageNamePrefixLength]
 	}
 
+	annotations := make(map[string]string, 1)
+	// Put actor information to track on the controller side
+	if u, ok := user.InfoFromContext(ctx); ok {
+		annotations[kargoapi.AnnotationKeyCreateActor] = kargoapi.FormatEventUserActor(u)
+	}
+
 	// ulid.Make() is pseudo-random, not crypto-random, but we don't care.
 	// We just want a unique ID that can be sorted lexicographically
 	promoName := strings.ToLower(fmt.Sprintf("%s.%s.%s", shortStageName, ulid.Make(), shortHash))
 
 	promotion := kargoapi.Promotion{
 		ObjectMeta: metav1.ObjectMeta{
-			Name:      promoName,
-			Namespace: stage.Namespace,
+			Name:        promoName,
+			Namespace:   stage.Namespace,
+			Annotations: annotations,
 		},
 		Spec: &kargoapi.PromotionSpec{
 			Stage:   stage.Name,
diff --git a/internal/webhook/promotion/webhook.go b/internal/webhook/promotion/webhook.go
index 579f390a..69b3c174 100644
--- a/internal/webhook/promotion/webhook.go
+++ b/internal/webhook/promotion/webhook.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"fmt"
 
+	admissionv1 "k8s.io/api/admission/v1"
 	authzv1 "k8s.io/api/authorization/v1"
 	corev1 "k8s.io/api/core/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -35,7 +36,8 @@ var (
 )
 
 type webhook struct {
-	client client.Client
+	client  client.Client
+	decoder *admission.Decoder
 
 	recorder record.EventRecorder
 
@@ -85,6 +87,7 @@ func SetupWebhookWithManager(
 	w := newWebhook(
 		cfg,
 		mgr.GetClient(),
+		admission.NewDecoder(mgr.GetScheme()),
 		libEvent.NewRecorder(ctx, mgr.GetScheme(), mgr.GetClient(), "promotion-webhook"),
 	)
 	return ctrl.NewWebhookManagedBy(mgr).
@@ -97,10 +100,12 @@ func SetupWebhookWithManager(
 func newWebhook(
 	cfg libWebhook.Config,
 	kubeClient client.Client,
+	decoder *admission.Decoder,
 	recorder record.EventRecorder,
 ) *webhook {
 	w := &webhook{
 		client:   kubeClient,
+		decoder:  decoder,
 		recorder: recorder,
 	}
 	w.getFreightFn = kargoapi.GetFreight
@@ -115,7 +120,40 @@ func newWebhook(
 }
 
 func (w *webhook) Default(ctx context.Context, obj runtime.Object) error {
+	req, err := w.admissionRequestFromContextFn(ctx)
+	if err != nil {
+		return fmt.Errorf("get admission request from context: %w", err)
+	}
+
 	promo := obj.(*kargoapi.Promotion) // nolint: forcetypeassert
+	var oldPromo *kargoapi.Promotion
+	// We need to decode old object manually since controller-runtime doesn't decode it for us.
+	if req.Operation == admissionv1.Update {
+		oldPromo = &kargoapi.Promotion{}
+		if err = w.decoder.DecodeRaw(req.OldObject, oldPromo); err != nil {
+			return fmt.Errorf("decode old object: %w", err)
+		}
+	}
+
+	if promo.Annotations == nil {
+		promo.Annotations = make(map[string]string, 1)
+	}
+	if req.Operation == admissionv1.Create {
+		// Set actor as an admission request's user info when the promotion is created
+		// to allow controllers to track who created it.
+		if !w.isRequestFromKargoControlplaneFn(req) {
+			promo.Annotations[kargoapi.AnnotationKeyCreateActor] =
+				kargoapi.FormatEventKubernetesUserActor(req.UserInfo)
+		}
+	} else if req.Operation == admissionv1.Update {
+		// Ensure actor annotation immutability
+		if oldActor, ok := oldPromo.Annotations[kargoapi.AnnotationKeyCreateActor]; ok {
+			promo.Annotations[kargoapi.AnnotationKeyCreateActor] = oldActor
+		} else {
+			delete(promo.Annotations, kargoapi.AnnotationKeyCreateActor)
+		}
+	}
+
 	stage, err := w.getStageFn(
 		ctx,
 		w.client,


Test patch:
diff --git a/internal/kargo/kargo_test.go b/internal/kargo/kargo_test.go
index 7ef06e47..cca70658 100644
--- a/internal/kargo/kargo_test.go
+++ b/internal/kargo/kargo_test.go
@@ -1,6 +1,7 @@
 package kargo
 
 import (
+	"context"
 	"strings"
 	"testing"
 
@@ -64,7 +65,7 @@ func TestNewPromotion(t *testing.T) {
 	for _, tc := range testCases {
 		tc := tc
 		t.Run(tc.name, func(t *testing.T) {
-			promo := NewPromotion(tc.stage, tc.freight)
+			promo := NewPromotion(context.TODO(), tc.stage, tc.freight)
 			require.Equal(t, tc.freight, promo.Spec.Freight)
 			require.Equal(t, tc.stage.Name, promo.Spec.Stage)
 			require.Equal(t, tc.freight, promo.Spec.Freight)
diff --git a/internal/webhook/promotion/webhook_test.go b/internal/webhook/promotion/webhook_test.go
index 33479792..cf7e66f3 100644
--- a/internal/webhook/promotion/webhook_test.go
+++ b/internal/webhook/promotion/webhook_test.go
@@ -27,6 +27,7 @@ func TestNewWebhook(t *testing.T) {
 	w := newWebhook(
 		libWebhook.Config{},
 		kubeClient,
+		admission.NewDecoder(kubeClient.Scheme()),
 		&fakeevent.EventRecorder{},
 	)
 	// Assert that all overridable behaviors were initialized to a default:
@@ -48,6 +49,9 @@ func TestDefault(t *testing.T) {
 		{
 			name: "error getting stage",
 			webhook: &webhook{
+				admissionRequestFromContextFn: func(context.Context) (admission.Request, error) {
+					return admission.Request{}, nil
+				},
 				getStageFn: func(
 					context.Context,
 					client.Client,
@@ -64,6 +68,9 @@ func TestDefault(t *testing.T) {
 		{
 			name: "stage not found",
 			webhook: &webhook{
+				admissionRequestFromContextFn: func(context.Context) (admission.Request, error) {
+					return admission.Request{}, nil
+				},
 				getStageFn: func(
 					context.Context,
 					client.Client,
@@ -80,6 +87,9 @@ func TestDefault(t *testing.T) {
 		{
 			name: "stage without promotion mechanisms",
 			webhook: &webhook{
+				admissionRequestFromContextFn: func(context.Context) (admission.Request, error) {
+					return admission.Request{}, nil
+				},
 				getStageFn: func(
 					context.Context,
 					client.Client,
@@ -98,6 +108,9 @@ func TestDefault(t *testing.T) {
 		{
 			name: "success",
 			webhook: &webhook{
+				admissionRequestFromContextFn: func(context.Context) (admission.Request, error) {
+					return admission.Request{}, nil
+				},
 				getStageFn: func(
 					context.Context,
 					client.Client,