Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package kargo-cli for openSUSE:Factory 
checked in at 2026-10-07 19:34:00
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/kargo-cli (Old)
 and      /work/SRC/openSUSE:Factory/.kargo-cli.new.3698736 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "kargo-cli"

Wed Oct  7 19:34:00 2026 rev:65 rq:1382923 version:1.12.2

Changes:
--------
--- /work/SRC/openSUSE:Factory/kargo-cli/kargo-cli.changes      2026-10-05 
13:03:37.422849552 +0200
+++ /work/SRC/openSUSE:Factory/.kargo-cli.new.3698736/kargo-cli.changes 
2026-10-07 19:34:29.052355035 +0200
@@ -1,0 +2,6 @@
+Wed Oct 07 05:39:37 UTC 2026 - Johannes Kastl 
<[email protected]>
+
+- Update to version 1.12.2:
+  No CLI-related changes or dependency updates
+
+-------------------------------------------------------------------

Old:
----
  kargo-cli-1.12.1.obscpio

New:
----
  kargo-cli-1.12.2.obscpio

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ kargo-cli.spec ++++++
--- /var/tmp/diff_new_pack.QotTKH/_old  2026-10-07 19:34:31.164443976 +0200
+++ /var/tmp/diff_new_pack.QotTKH/_new  2026-10-07 19:34:31.167444102 +0200
@@ -19,7 +19,7 @@
 %define executable_name kargo
 
 Name:           kargo-cli
-Version:        1.12.1
+Version:        1.12.2
 Release:        0
 Summary:        CLI for the Kubernetes Application lifecycle orchestration
 License:        Apache-2.0

++++++ _service ++++++
--- /var/tmp/diff_new_pack.QotTKH/_old  2026-10-07 19:34:31.210445913 +0200
+++ /var/tmp/diff_new_pack.QotTKH/_new  2026-10-07 19:34:31.213446039 +0200
@@ -3,7 +3,7 @@
     <param name="url">https://github.com/akuity/kargo.git</param>
     <param name="scm">git</param>
     <param name="exclude">.git</param>
-    <param name="revision">refs/tags/v1.12.1</param>
+    <param name="revision">refs/tags/v1.12.2</param>
     <param name="versionformat">@PARENT_TAG@</param>
     <param name="versionrewrite-pattern">v(.*)</param>
     <param name="changesgenerate">enable</param>

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.QotTKH/_old  2026-10-07 19:34:31.242447260 +0200
+++ /var/tmp/diff_new_pack.QotTKH/_new  2026-10-07 19:34:31.247447471 +0200
@@ -3,6 +3,6 @@
                 <param name="url">https://github.com/akuity/kargo</param>
               <param 
name="changesrevision">17077e406ed309fcc830d08776d9b9a47fe6e134</param></service><service
 name="tar_scm">
                 <param name="url">https://github.com/akuity/kargo.git</param>
-              <param 
name="changesrevision">c2316a2ffd24e9263783099b5238c3c4fda35707</param></service></servicedata>
+              <param 
name="changesrevision">2b5f3a2f12c1eb6156253ba71d44818d4262c865</param></service></servicedata>
 (No newline at EOF)
 

++++++ kargo-cli-1.12.1.obscpio -> kargo-cli-1.12.2.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kargo-cli-1.12.1/charts/kargo/README.md 
new/kargo-cli-1.12.2/charts/kargo/README.md
--- old/kargo-cli-1.12.1/charts/kargo/README.md 2026-10-02 22:45:24.000000000 
+0200
+++ new/kargo-cli-1.12.2/charts/kargo/README.md 2026-10-06 20:24:02.000000000 
+0200
@@ -115,6 +115,10 @@
 | `api.logFormat`                                   | The format of logs from 
the API server. Valid options are CONSOLE or JSON (case insensitive).           
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                            | 
`CONSOLE`                |
 | `api.secretManagementEnabled`                     | Specifies whether Secret 
management is enabled. This affects the API server's ability to manage 
repository credentials and other Project-level Secrets, such as those used by 
AnalysisRuns for verification purposes. If using GitOps to manage Kargo 
Projects declaratively, the API's Secret management capabilities are not needed 
and can be disabled to effectively reduce the API server's attackable surface.  
                                                                                
                                                                                
                                                                                
              | `true`                   |
 | `api.permissiveCORSPolicyEnabled`                 | Whether to enable a 
permissive CORS (Cross Origin Resource Sharing) policy. This is sometimes 
advantageous during local development, but otherwise, should generally be left 
disabled.                                                                       
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
       | `false`                  |
+| `api.trustedProxies`                              | IP addresses or CIDRs of 
proxies (e.g. an ingress controller or load balancer) in front of the API 
server. When a request arrives from one of these, the client's IP address is 
taken from `api.clientIPHeader`, or else from the rightmost `X-Forwarded-For` 
entry that is not a trusted proxy. When empty, the client's IP address is 
always the address the request arrived from.                                    
                                                                                
                                                                                
                                                                                
            | `[]`                     |
+| `api.clientIPHeader`                              | A header that every 
trusted proxy sets to the client's IP address, e.g. `CF-Connecting-IP` or 
`X-Real-IP`. Only honored on requests arriving from a trusted proxy.            
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
      | `""`                     |
+| `api.requestLog.allEnabled`                       | Whether to log every API 
request at INFO level. By default, only refused requests (401 and 403) are 
logged at INFO level and server errors at ERROR level, while all other requests 
are logged at DEBUG level.                                                      
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
| `false`                  |
+| `api.requestLog.sourceIPEnabled`                  | Whether to include the 
IP address each request came from in the API server's logs. IP addresses may be 
considered personal data, so this is disabled by default.                       
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                             | 
`false`                  |
 | `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.                                                 
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
                                                                                
     | `""`                     |
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/kargo-cli-1.12.1/charts/kargo/templates/api/cluster-roles.yaml 
new/kargo-cli-1.12.2/charts/kargo/templates/api/cluster-roles.yaml
--- old/kargo-cli-1.12.1/charts/kargo/templates/api/cluster-roles.yaml  
2026-10-02 22:45:24.000000000 +0200
+++ new/kargo-cli-1.12.2/charts/kargo/templates/api/cluster-roles.yaml  
2026-10-06 20:24:02.000000000 +0200
@@ -110,6 +110,9 @@
 - apiGroups:
   - rbac.authorization.k8s.io
   resources:
+  # clusterrolebindings is read by the escalation check on ServiceAccounts
+  # carrying OIDC claim annotations.
+  - clusterrolebindings
   - clusterroles
   - rolebindings
   - roles
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/kargo-cli-1.12.1/charts/kargo/templates/api/configmap.yaml 
new/kargo-cli-1.12.2/charts/kargo/templates/api/configmap.yaml
--- old/kargo-cli-1.12.1/charts/kargo/templates/api/configmap.yaml      
2026-10-02 22:45:24.000000000 +0200
+++ new/kargo-cli-1.12.2/charts/kargo/templates/api/configmap.yaml      
2026-10-06 20:24:02.000000000 +0200
@@ -34,6 +34,18 @@
   SYSTEM_RESOURCES_NAMESPACE: {{ quote 
.Values.global.systemResources.namespace }}
   SHARED_RESOURCES_NAMESPACE: {{ quote 
.Values.global.sharedResources.namespace }}
   PERMISSIVE_CORS_POLICY_ENABLED: {{ quote 
.Values.api.permissiveCORSPolicyEnabled }}
+  {{- with .Values.api.trustedProxies }}
+  TRUSTED_PROXIES: {{ join "," . | quote }}
+  {{- end }}
+  {{- with .Values.api.clientIPHeader }}
+  CLIENT_IP_HEADER: {{ quote . }}
+  {{- end }}
+  {{- if .Values.api.requestLog.allEnabled }}
+  REQUEST_LOG_ALL_ENABLED: "true"
+  {{- end }}
+  {{- if .Values.api.requestLog.sourceIPEnabled }}
+  REQUEST_LOG_SOURCE_IP_ENABLED: "true"
+  {{- end }}
   {{- if .Values.api.defaultControllerName }}
   DEFAULT_CONTROLLER_NAME: {{ .Values.api.defaultControllerName | quote }}
   {{- end }}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kargo-cli-1.12.1/charts/kargo/tests/api_test.yaml 
new/kargo-cli-1.12.2/charts/kargo/tests/api_test.yaml
--- old/kargo-cli-1.12.1/charts/kargo/tests/api_test.yaml       2026-10-02 
22:45:24.000000000 +0200
+++ new/kargo-cli-1.12.2/charts/kargo/tests/api_test.yaml       2026-10-06 
20:24:02.000000000 +0200
@@ -131,6 +131,43 @@
       - notExists:
           path: data.SECRET_MANAGEMENT_ENABLED
 
+  - it: omits client IP and request log settings by default
+    asserts:
+      - notExists:
+          path: data.TRUSTED_PROXIES
+      - notExists:
+          path: data.CLIENT_IP_HEADER
+      - notExists:
+          path: data.REQUEST_LOG_ALL_ENABLED
+      - notExists:
+          path: data.REQUEST_LOG_SOURCE_IP_ENABLED
+
+  - it: configures trusted proxies and client IP header
+    set:
+      api.trustedProxies:
+        - 10.0.0.0/8
+        - 192.168.1.5
+      api.clientIPHeader: CF-Connecting-IP
+    asserts:
+      - equal:
+          path: data.TRUSTED_PROXIES
+          value: 10.0.0.0/8,192.168.1.5
+      - equal:
+          path: data.CLIENT_IP_HEADER
+          value: CF-Connecting-IP
+
+  - it: enables request log settings
+    set:
+      api.requestLog.allEnabled: true
+      api.requestLog.sourceIPEnabled: true
+    asserts:
+      - equal:
+          path: data.REQUEST_LOG_ALL_ENABLED
+          value: "true"
+      - equal:
+          path: data.REQUEST_LOG_SOURCE_IP_ENABLED
+          value: "true"
+
   - it: enables OIDC config when OIDC is turned on
     set:
       api.oidc.enabled: true
@@ -269,3 +306,22 @@
               - targets
             verbs:
               - "*"
+  - it: grants read access to RBAC resources
+    documentSelector:
+      path: metadata.name
+      value: kargo-api
+    asserts:
+      - contains:
+          path: rules
+          content:
+            apiGroups:
+              - rbac.authorization.k8s.io
+            resources:
+              - clusterrolebindings
+              - clusterroles
+              - rolebindings
+              - roles
+            verbs:
+              - get
+              - list
+              - watch
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kargo-cli-1.12.1/charts/kargo/values.yaml 
new/kargo-cli-1.12.2/charts/kargo/values.yaml
--- old/kargo-cli-1.12.1/charts/kargo/values.yaml       2026-10-02 
22:45:24.000000000 +0200
+++ new/kargo-cli-1.12.2/charts/kargo/values.yaml       2026-10-06 
20:24:02.000000000 +0200
@@ -184,6 +184,17 @@
   ## @param api.permissiveCORSPolicyEnabled Whether to enable a permissive 
CORS (Cross Origin Resource Sharing) policy. This is sometimes advantageous 
during local development, but otherwise, should generally be left disabled.
   permissiveCORSPolicyEnabled: false
 
+  ## @param api.trustedProxies IP addresses or CIDRs of proxies (e.g. an 
ingress controller or load balancer) in front of the API server. When a request 
arrives from one of these, the client's IP address is taken from 
`api.clientIPHeader`, or else from the rightmost `X-Forwarded-For` entry that 
is not a trusted proxy. When empty, the client's IP address is always the 
address the request arrived from.
+  trustedProxies: []
+  ## @param api.clientIPHeader A header that every trusted proxy sets to the 
client's IP address, e.g. `CF-Connecting-IP` or `X-Real-IP`. Only honored on 
requests arriving from a trusted proxy.
+  clientIPHeader: ""
+
+  requestLog:
+    ## @param api.requestLog.allEnabled Whether to log every API request at 
INFO level. By default, only refused requests (401 and 403) are logged at INFO 
level and server errors at ERROR level, while all other requests are logged at 
DEBUG level.
+    allEnabled: false
+    ## @param api.requestLog.sourceIPEnabled Whether to include the IP address 
each request came from in the API server's logs. IP addresses may be considered 
personal data, so this is disabled by default.
+    sourceIPEnabled: false
+
   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.
     name: ""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/kargo-cli-1.12.1/docs/docs/40-operator-guide/20-advanced-installation/30-common-configurations.md
 
new/kargo-cli-1.12.2/docs/docs/40-operator-guide/20-advanced-installation/30-common-configurations.md
--- 
old/kargo-cli-1.12.1/docs/docs/40-operator-guide/20-advanced-installation/30-common-configurations.md
       2026-10-02 22:45:24.000000000 +0200
+++ 
new/kargo-cli-1.12.2/docs/docs/40-operator-guide/20-advanced-installation/30-common-configurations.md
       2026-10-06 20:24:02.000000000 +0200
@@ -240,6 +240,103 @@
 with the name `kargo-api-ingress-cert` to exist in the same namespace as the
 API server.
 
+### API Request Logs
+
+The API server writes one log line per REST API request, naming the method,
+path, status, duration, and the actor the request was authenticated as. Server
+errors are logged at `ERROR` level and refused requests (`401` and `403`) at 
`INFO`
+level. Everything else is logged at `DEBUG` level, so with the default
+`api.logLevel` of `INFO`, routine requests are not logged.
+
+To log every request at `INFO` level, without enabling debug logging for the
+rest of the API server, set the following configuration:
+
+```yaml
+api:
+  requestLog:
+    allEnabled: true
+```
+
+#### Authentication Failures
+
+A failed attempt to log in as the admin user is logged at `INFO` level as
+`refused request`, with a status of `403` and the reason as `error`. A
+successful one is logged at `INFO` level as `admin login successful`. The
+password is never logged.
+
+With OpenID Connect, logging in happens between the client and the identity
+provider, so the API server never sees a failed login there. Those are recorded
+by the identity provider (or by Dex, if enabled). What the API server does see
+is the token presented with each request. A request whose token is missing,
+expired, or fails verification is logged at `INFO` level as `refused request`
+with a status of `401` and the reason as `error`.
+
+#### Source IP Logging
+
+By default, request logs do not include the address a request came from, since
+the API server is typically behind a proxy that can log the same thing. To
+include it, set the following configuration:
+
+```yaml
+api:
+  requestLog:
+    sourceIPEnabled: true
+```
+
+Each request log line, and anything else logged while handling the request,
+then gains up to two fields:
+
+- `sourceIP`: The address of the client, as far as the API server can establish
+  it. Without [trusted proxies](#api-trusted-proxies), this is the address the
+  request arrived from, which clients cannot choose.
+- `forwardedFor`: The `X-Forwarded-For` chain the request arrived with, if any.
+  It is logged as received and is only as trustworthy as whatever sent it.
+
+:::note
+
+IP addresses are personal data under several privacy regimes, which is why this
+is opt-in. Check your retention obligations before enabling it.
+
+:::
+
+### API Trusted Proxies
+
+If the API server is behind an ingress controller, a load balancer, or a 
service
+mesh sidecar, the address a request arrives from is that proxy's. For the API
+server to determine the client's address instead (for example, for
+[source IP logging](#source-ip-logging)), list the proxies as trusted:
+
+```yaml
+api:
+  # The addresses of your ingress controller or load balancer, not the whole
+  # Pod network.
+  trustedProxies:
+    - 10.0.12.34
+    - 10.0.13.0/28
+  # Only if every trusted proxy sets or overwrites this header.
+  clientIPHeader: CF-Connecting-IP
+```
+
+When a request arrives from a trusted proxy, the client's address is the value
+of the client IP header, if one is configured and the request carries a valid
+address in it. Otherwise, it is the rightmost `X-Forwarded-For` entry that is
+not a trusted proxy. Every entry to the right of that one was added by a proxy
+you trust, so the client cannot have chosen it. Requests arriving from any 
other
+address are attributed to that address, so clients that bypass your proxies
+cannot use either header to pick their own address.
+
+:::caution
+
+Every address you trust can choose the client address of the requests it sends.
+Trusting a broad range, such as the whole Pod network, lets any Pod that calls
+the API server directly choose its own.
+
+Likewise, only name a client IP header that every trusted proxy sets or
+overwrites. A proxy that passes the header through from the client lets the
+client choose its own address.
+
+:::
+
 ## Git Configuration
 
 Kargo supports a number of Git-related configurations that can be set at
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/kargo-cli-1.12.1/docs/docs/50-user-guide/20-how-to-guides/45-promotion-windows.md
 
new/kargo-cli-1.12.2/docs/docs/50-user-guide/20-how-to-guides/45-promotion-windows.md
--- 
old/kargo-cli-1.12.1/docs/docs/50-user-guide/20-how-to-guides/45-promotion-windows.md
       2026-10-02 22:45:24.000000000 +0200
+++ 
new/kargo-cli-1.12.2/docs/docs/50-user-guide/20-how-to-guides/45-promotion-windows.md
       2026-10-06 20:24:02.000000000 +0200
@@ -263,6 +263,108 @@
 ![Example schedule](./img/promotion-window-diagram.png)
 
 
+## Using Promotion Windows in the Kargo UI
+
+Kargo Enterprise adds a <hlt>Promotion Windows</hlt> view to the UI, so windows
+can be managed without hand-writing iCal values. Everything the view does is
+read from and written to the same `promotionWindows` field on `ProjectConfig`
+and `ClusterConfig` described above — the UI is an alternative editor for that
+configuration, not a separate mechanism.
+
+### Where to Find It
+
+There are two views, one per scope:
+
+| Scope | Where |
+|---|---|
+| **Cluster promotion windows** | 
`/settings/cluster-config/promotion-windows`, or in cluster <hlt>Settings</hlt> 
under <hlt>Cluster Config</hlt> → <hlt>Promotion Windows</hlt> |
+| **Project promotion windows** | 
`/project/<project>/settings/project-config/promotion-windows`, or in a 
Project's <hlt>Settings</hlt> under <hlt>ProjectConfig</hlt> → <hlt>Promotion 
Windows</hlt> |
+
+The cluster view edits the one `ClusterConfig` named `cluster`; the Project 
view
+edits that Project's `ProjectConfig`. The only difference between them is that
+the cluster view additionally exposes `projectSelector`.
+
+
+### Calendar and List Views
+
+Each view offers two tabs: <hlt>Calendar</hlt> and <hlt>List</hlt>.
+
+#### Calendar
+
+The <hlt>Calendar</hlt> tab lays the windows out as a month grid. Use
+<hlt>Today</hlt> and the arrows to move between months.
+
+![Promotion windows calendar view](./img/calendar-view-promotion-window.png)
+
+Each entry resolves `dtstart`, `dtend`, `rrule` and the time zone into the
+concrete occurrences that fall in the displayed month, so you see the schedule
+the rule actually produces rather than the rule itself. Green entries are
+`Allow` windows, red are `Deny`, and dashed entries are disabled windows.
+
+Clicking an occurrence opens the window that produced it for editing; clicking
+an empty day starts a new one-hour window drafted on that day.
+
+:::note
+
+The calendar renders each window's own occurrences. It does not compute a
+`Stage`'s net window status, so where an `Allow` and a `Deny` occurrence
+overlap, the [precedence rules](#allow-and-deny-precedence) still decide the
+outcome — `Deny` wins. Consult a `Stage`'s
+[`status.promotionWindowStatus`](#stage-status) for its effective state.
+
+:::
+
+#### List
+
+The <hlt>List</hlt> tab shows the configured windows themselves rather than
+their occurrences.
+
+![Promotion windows list view](./img/promotion-windows-list-view.png)
+
+### Creating and Editing a Window
+
+<hlt>+ New Window</hlt> opens a modal with every window field. The same modal
+opens, titled <hlt>Edit promotion window</hlt>, when you edit an existing
+window.
+
+![Promotion window configuration](./img/promotion-window-config.png)
+
+Saving writes the complete window list back to the `ProjectConfig` or
+`ClusterConfig` immediately — there is no separate save step on the page — and
+the UI confirms with a <hlt>Promotion windows updated</hlt> message. When
+editing, <hlt>Delete</hlt> in the modal's footer removes the window after a
+confirmation prompt.
+
+### Restricting Scope
+
+Both selectors start at "Every `Stage`" (and "Every Project" in the cluster
+view), which leaves the corresponding field unset.
+<hlt>Restrict to specific Stages</hlt> reveals the selector controls:
+
+![Promotion window Stage 
selection](./img/promotion-window-config-stage-selection.png)
+
+- **<hlt>Name</hlt>** — a name pattern, with the dropdown choosing the matching
+  mode: <hlt>Exact</hlt> for a literal name, <hlt>glob:</hlt> for shell-style
+  wildcards such as `prod-*`, or <hlt>regex:</hlt> for an RE2 expression
+  (unanchored by default).
+- **<hlt>Labels</hlt>** — key/value pairs that become `matchLabels`.
+- **<hlt>Label expressions</hlt>** — key, operator and values, becoming
+  `matchExpressions`. The values field is disabled for operators that take 
none,
+  such as `Exists`.
+
+Constraints are ANDed: a `Stage` must satisfy the name pattern *and* every 
label
+*and* every expression to be covered by the window.
+<hlt>Clear and apply to every Stage</hlt> discards the constraints and returns
+the window to covering everything in scope.
+
+:::warning
+
+Pattern and label matching in the UI carries the same security considerations
+described in [Selecting Stages](#selecting-stages). Exact names remain the most
+predictable option.
+
+:::
+
 ## Effect on Promotions
 
 Promotion windows gate all promotions uniformly — automatic, manual, and
Binary files 
old/kargo-cli-1.12.1/docs/docs/50-user-guide/20-how-to-guides/img/calendar-view-promotion-window.png
 and 
new/kargo-cli-1.12.2/docs/docs/50-user-guide/20-how-to-guides/img/calendar-view-promotion-window.png
 differ
Binary files 
old/kargo-cli-1.12.1/docs/docs/50-user-guide/20-how-to-guides/img/promotion-window-config-stage-selection.png
 and 
new/kargo-cli-1.12.2/docs/docs/50-user-guide/20-how-to-guides/img/promotion-window-config-stage-selection.png
 differ
Binary files 
old/kargo-cli-1.12.1/docs/docs/50-user-guide/20-how-to-guides/img/promotion-window-config.png
 and 
new/kargo-cli-1.12.2/docs/docs/50-user-guide/20-how-to-guides/img/promotion-window-config.png
 differ
Binary files 
old/kargo-cli-1.12.1/docs/docs/50-user-guide/20-how-to-guides/img/promotion-windows-list-view.png
 and 
new/kargo-cli-1.12.2/docs/docs/50-user-guide/20-how-to-guides/img/promotion-windows-list-view.png
 differ
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/kargo-cli-1.12.1/pkg/controller/management/projects/projects.go 
new/kargo-cli-1.12.2/pkg/controller/management/projects/projects.go
--- old/kargo-cli-1.12.1/pkg/controller/management/projects/projects.go 
2026-10-02 22:45:24.000000000 +0200
+++ new/kargo-cli-1.12.2/pkg/controller/management/projects/projects.go 
2026-10-06 20:24:02.000000000 +0200
@@ -1000,8 +1000,15 @@
                                },
                                { // View access to Kargo resources
                                        APIGroups: 
[]string{kargoapi.GroupVersion.Group},
-                                       Resources: []string{"freights", 
"stages", "warehouses", "projectconfigs"},
-                                       Verbs:     []string{"get", "list", 
"watch"},
+                                       Resources: []string{
+                                               "freights",
+                                               "projectconfigs",
+                                               "promotionrequests",
+                                               "stages",
+                                               "targets",
+                                               "warehouses",
+                                       },
+                                       Verbs: []string{"get", "list", "watch"},
                                },
                                { // Promote permission on all stages
                                        APIGroups: 
[]string{kargoapi.GroupVersion.Group},
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/kargo-cli-1.12.1/pkg/controller/management/projects/projects_test.go 
new/kargo-cli-1.12.2/pkg/controller/management/projects/projects_test.go
--- old/kargo-cli-1.12.1/pkg/controller/management/projects/projects_test.go    
2026-10-02 22:45:24.000000000 +0200
+++ new/kargo-cli-1.12.2/pkg/controller/management/projects/projects_test.go    
2026-10-06 20:24:02.000000000 +0200
@@ -2702,6 +2702,11 @@
                        verbs:    []string{"*"},
                },
                {
+                       roleName: "kargo-promoter",
+                       resource: "targets",
+                       verbs:    []string{"get", "list", "watch"},
+               },
+               {
                        roleName: "kargo-viewer",
                        resource: "targets",
                        verbs:    []string{"get", "list", "watch"},
@@ -2724,7 +2729,7 @@
        }
        // PromotionRequests are readable but never writable by a user: only the
        // Stage controller and the API server create them.
-       for _, roleName := range []string{"kargo-admin", "kargo-viewer"} {
+       for _, roleName := range []string{"kargo-admin", "kargo-promoter", 
"kargo-viewer"} {
                t.Run(roleName+"/promotionrequests read-only", func(t 
*testing.T) {
                        role := createdRoles[roleName]
                        require.NotNil(t, role)
@@ -2739,20 +2744,6 @@
                        require.True(t, found, "no promotionrequests rule 
found")
                })
        }
-       t.Run("kargo-promoter/no promotionrequests", func(t *testing.T) {
-               role := createdRoles["kargo-promoter"]
-               require.NotNil(t, role)
-               for _, rule := range role.Rules {
-                       require.NotContains(t, rule.Resources, 
"promotionrequests")
-               }
-       })
-       t.Run("kargo-promoter/no targets", func(t *testing.T) {
-               role := createdRoles["kargo-promoter"]
-               require.NotNil(t, role)
-               for _, rule := range role.Rules {
-                       require.NotContains(t, rule.Resources, "targets")
-               }
-       })
        // Regression test for GHSA-rx5g-3338-f2mf: patch on freights/status 
let a
        // holder write status.verifiedIn directly, bypassing verification/soak
        // requirements without ever needing the promote verb. Freight approval 
now
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kargo-cli-1.12.1/pkg/server/admin_login.go 
new/kargo-cli-1.12.2/pkg/server/admin_login.go
--- old/kargo-cli-1.12.1/pkg/server/admin_login.go      2026-10-02 
22:45:24.000000000 +0200
+++ new/kargo-cli-1.12.2/pkg/server/admin_login.go      2026-10-06 
20:24:02.000000000 +0200
@@ -12,6 +12,7 @@
        "golang.org/x/crypto/bcrypt"
 
        libhttp "github.com/akuity/kargo/pkg/http"
+       "github.com/akuity/kargo/pkg/logging"
 )
 
 func init() {
@@ -87,6 +88,10 @@
                return
        }
 
+       // Failures need no record of their own; the request logging middleware
+       // records them as refused requests.
+       logging.LoggerFromContext(c.Request.Context()).Info("admin login 
successful")
+
        c.JSON(http.StatusOK, adminLoginResponse{
                IDToken: signedToken,
        })
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kargo-cli-1.12.1/pkg/server/admin_login_test.go 
new/kargo-cli-1.12.2/pkg/server/admin_login_test.go
--- old/kargo-cli-1.12.1/pkg/server/admin_login_test.go 2026-10-02 
22:45:24.000000000 +0200
+++ new/kargo-cli-1.12.2/pkg/server/admin_login_test.go 2026-10-06 
20:24:02.000000000 +0200
@@ -6,10 +6,15 @@
        "testing"
        "time"
 
+       "github.com/gin-gonic/gin"
        "github.com/stretchr/testify/require"
+       "go.uber.org/zap"
+       "go.uber.org/zap/zapcore"
+       "go.uber.org/zap/zaptest/observer"
        "golang.org/x/crypto/bcrypt"
        "sigs.k8s.io/controller-runtime/pkg/client"
 
+       "github.com/akuity/kargo/pkg/logging"
        "github.com/akuity/kargo/pkg/server/config"
 )
 
@@ -69,3 +74,63 @@
                },
        )
 }
+
+func Test_server_adminLogin_logsSuccess(t *testing.T) {
+       const testPassword = "admin-password"
+       hashedPassword, err := bcrypt.GenerateFromPassword(
+               []byte(testPassword),
+               bcrypt.DefaultCost,
+       )
+       require.NoError(t, err)
+       s := &server{cfg: config.ServerConfig{
+               AdminConfig: &config.AdminConfig{
+                       HashedPassword:  string(hashedPassword),
+                       TokenIssuer:     "test-issuer",
+                       TokenAudience:   "test-audience",
+                       TokenTTL:        time.Hour,
+                       TokenSigningKey: []byte("test-key"),
+               },
+       }}
+
+       testCases := []struct {
+               name       string
+               password   string
+               assertions func(t *testing.T, entries []observer.LoggedEntry)
+       }{
+               {
+                       name:     "success is logged",
+                       password: testPassword,
+                       assertions: func(t *testing.T, entries 
[]observer.LoggedEntry) {
+                               require.Len(t, entries, 1)
+                               require.Equal(t, zapcore.InfoLevel, 
entries[0].Level)
+                               require.Equal(t, "admin login successful", 
entries[0].Message)
+                       },
+               },
+               {
+                       // The request logging middleware records failures.
+                       name:     "failure is not logged",
+                       password: "wrong-password",
+                       assertions: func(t *testing.T, entries 
[]observer.LoggedEntry) {
+                               require.Empty(t, entries)
+                       },
+               },
+       }
+       for _, testCase := range testCases {
+               t.Run(testCase.name, func(t *testing.T) {
+                       t.Parallel()
+                       core, recorded := observer.New(zapcore.InfoLevel)
+                       req := httptest.NewRequest(http.MethodPost, 
"/v1beta1/login", nil)
+                       req = req.WithContext(logging.ContextWithLogger(
+                               req.Context(),
+                               logging.Wrap(zap.New(core)),
+                       ))
+                       req.Header.Set("Authorization", "Bearer 
"+testCase.password)
+                       c, _ := gin.CreateTestContext(httptest.NewRecorder())
+                       c.Request = req
+
+                       s.adminLogin(c)
+
+                       testCase.assertions(t, recorded.All())
+               })
+       }
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kargo-cli-1.12.1/pkg/server/config/config.go 
new/kargo-cli-1.12.2/pkg/server/config/config.go
--- old/kargo-cli-1.12.1/pkg/server/config/config.go    2026-10-02 
22:45:24.000000000 +0200
+++ new/kargo-cli-1.12.2/pkg/server/config/config.go    2026-10-06 
20:24:02.000000000 +0200
@@ -59,6 +59,22 @@
        // know which controller's liveness to associate with such Stages. The 
default
        // controller is often unnamed, so an empty string is a valid value.
        DefaultControllerName string
+       // TrustedProxies are the IP addresses and CIDRs of proxies in front of 
the
+       // server whose forwarding headers are believed when determining the IP
+       // address a request came from. When empty, no forwarding header is
+       // believed.
+       TrustedProxies []string
+       // ClientIPHeader names a header that every trusted proxy sets to the
+       // client's IP address (e.g. CF-Connecting-IP). It is believed only on
+       // requests arriving from a trusted proxy and takes precedence over
+       // X-Forwarded-For.
+       ClientIPHeader string
+       // RequestLogAllEnabled indicates whether routine requests should be 
logged
+       // at info level instead of debug level.
+       RequestLogAllEnabled bool
+       // RequestLogSourceIPEnabled indicates whether the IP address each 
request
+       // came from should be logged.
+       RequestLogSourceIPEnabled bool
 
        // AdditionalHandlers is a map of path patterns to HTTP handlers that 
will
        // be registered on the server's HTTP mux alongside its own handlers. 
This
@@ -129,6 +145,16 @@
                "kargo-shared-resources",
        )
        cfg.KargoNamespace = os.GetEnv("KARGO_NAMESPACE", "kargo")
+       for proxy := range strings.SplitSeq(os.GetEnv("TRUSTED_PROXIES", ""), 
",") {
+               if proxy = strings.TrimSpace(proxy); proxy != "" {
+                       cfg.TrustedProxies = append(cfg.TrustedProxies, proxy)
+               }
+       }
+       cfg.ClientIPHeader = os.GetEnv("CLIENT_IP_HEADER", "")
+       cfg.RequestLogAllEnabled =
+               types.MustParseBool(os.GetEnv("REQUEST_LOG_ALL_ENABLED", 
"false"))
+       cfg.RequestLogSourceIPEnabled =
+               types.MustParseBool(os.GetEnv("REQUEST_LOG_SOURCE_IP_ENABLED", 
"false"))
        cfg.DefaultControllerName = os.GetEnv("DEFAULT_CONTROLLER_NAME", "")
        cfg.BasePath = NormalizeBasePath(os.GetEnv("API_BASE_PATH", ""))
        return cfg
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kargo-cli-1.12.1/pkg/server/engine.go 
new/kargo-cli-1.12.2/pkg/server/engine.go
--- old/kargo-cli-1.12.1/pkg/server/engine.go   1970-01-01 01:00:00.000000000 
+0100
+++ new/kargo-cli-1.12.2/pkg/server/engine.go   2026-10-06 20:24:02.000000000 
+0200
@@ -0,0 +1,34 @@
+package server
+
+import (
+       "fmt"
+
+       "github.com/gin-gonic/gin"
+
+       "github.com/akuity/kargo/pkg/server/config"
+)
+
+// ConfigureEngine applies the settings every Gin engine serving Kargo's API
+// must share, so that an engine built outside this package behaves like the
+// one built within it.
+//
+// Note: This sets Gin's mode, which is package-level state. Every engine in
+// the process is in release mode as a result, which is what we want of all of
+// them anyway.
+func ConfigureEngine(engine *gin.Engine, cfg config.ServerConfig) error {
+       // Suppresses Gin-related log noise that we don't want, even at 
development
+       // time.
+       gin.SetMode(gin.ReleaseMode)
+
+       // Gin trusts every proxy by default, which would let any client choose 
the
+       // IP address it is taken to have come from. An empty list trusts none.
+       if err := engine.SetTrustedProxies(cfg.TrustedProxies); err != nil {
+               return fmt.Errorf("error setting trusted proxies: %w", err)
+       }
+       // Consulted, in order, only for requests arriving from a trusted proxy.
+       engine.RemoteIPHeaders = []string{"X-Forwarded-For"}
+       if cfg.ClientIPHeader != "" {
+               engine.RemoteIPHeaders = []string{cfg.ClientIPHeader, 
"X-Forwarded-For"}
+       }
+       return nil
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kargo-cli-1.12.1/pkg/server/engine_test.go 
new/kargo-cli-1.12.2/pkg/server/engine_test.go
--- old/kargo-cli-1.12.1/pkg/server/engine_test.go      1970-01-01 
01:00:00.000000000 +0100
+++ new/kargo-cli-1.12.2/pkg/server/engine_test.go      2026-10-06 
20:24:02.000000000 +0200
@@ -0,0 +1,101 @@
+package server
+
+import (
+       "net/http"
+       "net/http/httptest"
+       "testing"
+
+       "github.com/gin-gonic/gin"
+       "github.com/stretchr/testify/require"
+
+       "github.com/akuity/kargo/pkg/server/config"
+)
+
+func TestConfigureEngine(t *testing.T) {
+       trusted := []string{"10.0.0.0/8", "127.0.0.1"}
+       testCases := []struct {
+               name       string
+               cfg        config.ServerConfig
+               remoteAddr string
+               headers    http.Header
+               expected   string
+       }{
+               {
+                       name:       "forwarding headers are ignored by default",
+                       remoteAddr: "203.0.113.7:4321",
+                       headers: http.Header{
+                               "X-Forwarded-For": {"198.51.100.1"},
+                               "X-Real-Ip":       {"198.51.100.2"},
+                       },
+                       expected: "203.0.113.7",
+               },
+               {
+                       name:       "forwarding headers from an untrusted peer 
are ignored",
+                       cfg:        config.ServerConfig{TrustedProxies: 
trusted, ClientIPHeader: "CF-Connecting-IP"},
+                       remoteAddr: "203.0.113.7:4321",
+                       headers: http.Header{
+                               "X-Forwarded-For":  {"198.51.100.1"},
+                               "Cf-Connecting-Ip": {"198.51.100.2"},
+                       },
+                       expected: "203.0.113.7",
+               },
+               {
+                       name:       "rightmost forwarded entry that is not a 
trusted proxy",
+                       cfg:        config.ServerConfig{TrustedProxies: 
trusted},
+                       remoteAddr: "10.0.0.1:4321",
+                       // The leftmost entry is whatever the client claimed.
+                       headers:  http.Header{"X-Forwarded-For": 
{"198.51.100.1, 203.0.113.7, 10.0.0.2"}},
+                       expected: "203.0.113.7",
+               },
+               {
+                       name:       "X-Real-IP is not believed",
+                       cfg:        config.ServerConfig{TrustedProxies: 
trusted},
+                       remoteAddr: "10.0.0.1:4321",
+                       headers:    http.Header{"X-Real-Ip": {"198.51.100.2"}},
+                       expected:   "10.0.0.1",
+               },
+               {
+                       name:       "client IP header from a trusted peer wins",
+                       cfg:        config.ServerConfig{TrustedProxies: 
trusted, ClientIPHeader: "CF-Connecting-IP"},
+                       remoteAddr: "10.0.0.1:4321",
+                       headers: http.Header{
+                               "X-Forwarded-For":  {"203.0.113.7"},
+                               "Cf-Connecting-Ip": {"198.51.100.2"},
+                       },
+                       expected: "198.51.100.2",
+               },
+               {
+                       name:       "invalid client IP header falls back to 
forwarded chain",
+                       cfg:        config.ServerConfig{TrustedProxies: 
trusted, ClientIPHeader: "CF-Connecting-IP"},
+                       remoteAddr: "10.0.0.1:4321",
+                       headers: http.Header{
+                               "X-Forwarded-For":  {"203.0.113.7"},
+                               "Cf-Connecting-Ip": {"garbage"},
+                       },
+                       expected: "203.0.113.7",
+               },
+       }
+       for _, testCase := range testCases {
+               t.Run(testCase.name, func(t *testing.T) {
+                       engine := gin.New()
+                       require.NoError(t, ConfigureEngine(engine, 
testCase.cfg))
+                       var clientIP string
+                       engine.GET("/", func(c *gin.Context) { clientIP = 
c.ClientIP() })
+
+                       req := httptest.NewRequest(http.MethodGet, "/", nil)
+                       req.RemoteAddr = testCase.remoteAddr
+                       req.Header = testCase.headers
+                       engine.ServeHTTP(httptest.NewRecorder(), req)
+
+                       require.Equal(t, testCase.expected, clientIP)
+               })
+       }
+
+       t.Run("invalid trusted proxy", func(t *testing.T) {
+               err := ConfigureEngine(
+                       gin.New(),
+                       config.ServerConfig{TrustedProxies: 
[]string{"proxy.example.com"}},
+               )
+               require.ErrorContains(t, err, "error setting trusted proxies")
+       })
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kargo-cli-1.12.1/pkg/server/kubernetes/client.go 
new/kargo-cli-1.12.2/pkg/server/kubernetes/client.go
--- old/kargo-cli-1.12.1/pkg/server/kubernetes/client.go        2026-10-02 
22:45:24.000000000 +0200
+++ new/kargo-cli-1.12.2/pkg/server/kubernetes/client.go        2026-10-06 
20:24:02.000000000 +0200
@@ -256,6 +256,14 @@
                                Cache: &libClient.CacheOptions{
                                        DisableFor: []libClient.Object{
                                                &corev1.Secret{},
+                                               // ClusterRoleBindings are read 
only by the escalation check
+                                               // on claim-annotated 
ServiceAccount writes, which is too rare
+                                               // to justify caching every 
binding in the cluster. Reading
+                                               // them uncached also means a 
missing list permission
+                                               // surfaces as an immediate 
Forbidden error; a cached read
+                                               // would instead block the 
request on an informer that can
+                                               // never sync.
+                                               &rbacv1.ClusterRoleBinding{},
                                        },
                                },
                        }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kargo-cli-1.12.1/pkg/server/logging_middleware.go 
new/kargo-cli-1.12.2/pkg/server/logging_middleware.go
--- old/kargo-cli-1.12.1/pkg/server/logging_middleware.go       2026-10-02 
22:45:24.000000000 +0200
+++ new/kargo-cli-1.12.2/pkg/server/logging_middleware.go       2026-10-06 
20:24:02.000000000 +0200
@@ -3,6 +3,7 @@
 import (
        "errors"
        "net/http"
+       "strings"
        "time"
 
        "github.com/gin-gonic/gin"
@@ -10,6 +11,7 @@
        kargoapi "github.com/akuity/kargo/api/v1alpha1"
        "github.com/akuity/kargo/pkg/api"
        "github.com/akuity/kargo/pkg/logging"
+       "github.com/akuity/kargo/pkg/server/config"
        "github.com/akuity/kargo/pkg/server/user"
 )
 
@@ -25,14 +27,35 @@
 // A server error is recorded at error level. A request that was refused is
 // recorded at info level, because a refusal is never routine and is the first
 // thing anyone looks for when a user reports being unable to do something.
-// Everything else is operational detail and recorded at debug level.
+// Everything else is operational detail and recorded at debug level, unless
+// cfg.RequestLogAllEnabled raises it to info level.
+//
+// The IP address a request came from is recorded only when
+// cfg.RequestLogSourceIPEnabled is set, because it is personal data and is
+// usually better recorded by whatever proxy sits in front of the server. It is
+// resolved by the engine, which ConfigureEngine must have configured.
 //
 // This must be the outermost middleware, so that the status and any reported
 // error it records are the ones the client actually received.
-func LoggingMiddleware() gin.HandlerFunc {
+func LoggingMiddleware(cfg config.ServerConfig) gin.HandlerFunc {
        return func(c *gin.Context) {
                start := time.Now()
 
+               // Taken on the way in and used for the line recorded below, so 
that line
+               // does not depend on what anything within does to the 
request's context.
+               ctx := c.Request.Context()
+               logger := logging.LoggerFromContext(ctx)
+               if cfg.RequestLogSourceIPEnabled {
+                       // Bound to the request's logger, so that everything 
logged on the
+                       // request's behalf says where it came from.
+                       logger = logger.WithValues("sourceIP", c.ClientIP())
+                       // As received, and so only as trustworthy as whatever 
sent it.
+                       if forwarded := 
c.Request.Header.Values("X-Forwarded-For"); len(forwarded) > 0 {
+                               logger = logger.WithValues("forwardedFor", 
strings.Join(forwarded, ", "))
+                       }
+                       c.Request = 
c.Request.WithContext(logging.ContextWithLogger(ctx, logger))
+               }
+
                c.Next()
 
                status := c.Writer.Status()
@@ -45,7 +68,7 @@
                // Without stack traces, because this middleware sits above 
every handler
                // and every other middleware, so a trace from here describes 
only the path
                // through Gin and reveals nothing about what went wrong.
-               logger := logging.LoggerFromContext(c.Request.Context()).
+               logger = logger.
                        WithoutStackTraces().
                        WithValues(
                                "method", c.Request.Method,
@@ -66,7 +89,11 @@
                                logger.Info("refused request")
                                return
                        }
-                       logger.Debug("handled request")
+                       if cfg.RequestLogAllEnabled {
+                               logger.Info("handled request")
+                       } else {
+                               logger.Debug("handled request")
+                       }
                        return
                }
                if reported == nil {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/kargo-cli-1.12.1/pkg/server/logging_middleware_test.go 
new/kargo-cli-1.12.2/pkg/server/logging_middleware_test.go
--- old/kargo-cli-1.12.1/pkg/server/logging_middleware_test.go  2026-10-02 
22:45:24.000000000 +0200
+++ new/kargo-cli-1.12.2/pkg/server/logging_middleware_test.go  2026-10-06 
20:24:02.000000000 +0200
@@ -15,6 +15,7 @@
        kargoapi "github.com/akuity/kargo/api/v1alpha1"
        libhttp "github.com/akuity/kargo/pkg/http"
        "github.com/akuity/kargo/pkg/logging"
+       "github.com/akuity/kargo/pkg/server/config"
        "github.com/akuity/kargo/pkg/server/user"
 )
 
@@ -26,6 +27,8 @@
                // level is the level the logger is configured with, so that 
what a given
                // LOG_LEVEL suppresses is observable.
                level          zapcore.Level
+               cfg            config.ServerConfig
+               headers        map[string]string
                handler        gin.HandlerFunc
                expectedStatus int
                assertions     func(t *testing.T, entries 
[]observer.LoggedEntry)
@@ -140,6 +143,92 @@
                                require.EqualValues(t, 
http.StatusInternalServerError, fields["status"])
                        },
                },
+               {
+                       name:           "routine request is recorded at info 
level when enabled",
+                       level:          zapcore.InfoLevel,
+                       cfg:            
config.ServerConfig{RequestLogAllEnabled: true},
+                       handler:        func(c *gin.Context) { 
c.Status(http.StatusOK) },
+                       expectedStatus: http.StatusOK,
+                       assertions: func(t *testing.T, entries 
[]observer.LoggedEntry) {
+                               require.Len(t, entries, 1)
+                               require.Equal(t, zapcore.InfoLevel, 
entries[0].Level)
+                               require.Equal(t, "handled request", 
entries[0].Message)
+                       },
+               },
+               {
+                       name:           "source IP is not recorded by default",
+                       level:          zapcore.DebugLevel,
+                       headers:        map[string]string{"X-Forwarded-For": 
"203.0.113.7"},
+                       handler:        func(c *gin.Context) { 
c.Status(http.StatusOK) },
+                       expectedStatus: http.StatusOK,
+                       assertions: func(t *testing.T, entries 
[]observer.LoggedEntry) {
+                               require.Len(t, entries, 1)
+                               fields := entries[0].ContextMap()
+                               require.NotContains(t, fields, "sourceIP")
+                               require.NotContains(t, fields, "forwardedFor")
+                       },
+               },
+               {
+                       name:           "source IP is recorded when enabled",
+                       level:          zapcore.DebugLevel,
+                       cfg:            
config.ServerConfig{RequestLogSourceIPEnabled: true},
+                       handler:        func(c *gin.Context) { 
c.Status(http.StatusOK) },
+                       expectedStatus: http.StatusOK,
+                       assertions: func(t *testing.T, entries 
[]observer.LoggedEntry) {
+                               require.Len(t, entries, 1)
+                               fields := entries[0].ContextMap()
+                               // The address httptest gives every request.
+                               require.Equal(t, "192.0.2.1", 
fields["sourceIP"])
+                               require.NotContains(t, fields, "forwardedFor")
+                       },
+               },
+               {
+                       name:  "source IP is on everything logged for the 
request",
+                       level: zapcore.DebugLevel,
+                       cfg:   config.ServerConfig{RequestLogSourceIPEnabled: 
true},
+                       handler: func(c *gin.Context) {
+                               
logging.LoggerFromContext(c.Request.Context()).Info("from the handler")
+                               c.Status(http.StatusOK)
+                       },
+                       expectedStatus: http.StatusOK,
+                       assertions: func(t *testing.T, entries 
[]observer.LoggedEntry) {
+                               require.Len(t, entries, 2)
+                               require.Equal(t, "from the handler", 
entries[0].Message)
+                               require.Equal(t, "192.0.2.1", 
entries[0].ContextMap()["sourceIP"])
+                               require.Equal(t, "192.0.2.1", 
entries[1].ContextMap()["sourceIP"])
+                       },
+               },
+               {
+                       name:  "source IP survives the request's logger being 
replaced",
+                       level: zapcore.DebugLevel,
+                       cfg:   config.ServerConfig{RequestLogSourceIPEnabled: 
true},
+                       handler: func(c *gin.Context) {
+                               c.Request = 
c.Request.WithContext(logging.ContextWithLogger(
+                                       c.Request.Context(),
+                                       logging.Wrap(zap.NewNop()),
+                               ))
+                               c.Status(http.StatusOK)
+                       },
+                       expectedStatus: http.StatusOK,
+                       assertions: func(t *testing.T, entries 
[]observer.LoggedEntry) {
+                               require.Len(t, entries, 1)
+                               require.Equal(t, "192.0.2.1", 
entries[0].ContextMap()["sourceIP"])
+                       },
+               },
+               {
+                       name:           "forwarded chain is recorded as 
received",
+                       level:          zapcore.DebugLevel,
+                       cfg:            
config.ServerConfig{RequestLogSourceIPEnabled: true},
+                       headers:        map[string]string{"X-Forwarded-For": 
"203.0.113.7, 10.0.0.1"},
+                       handler:        func(c *gin.Context) { 
c.Status(http.StatusOK) },
+                       expectedStatus: http.StatusOK,
+                       assertions: func(t *testing.T, entries 
[]observer.LoggedEntry) {
+                               require.Len(t, entries, 1)
+                               fields := entries[0].ContextMap()
+                               require.Equal(t, "192.0.2.1", 
fields["sourceIP"])
+                               require.Equal(t, "203.0.113.7, 10.0.0.1", 
fields["forwardedFor"])
+                       },
+               },
        }
 
        for _, testCase := range testCases {
@@ -151,18 +240,23 @@
 
                        s := &server{}
                        router := gin.New()
+                       require.NoError(t, ConfigureEngine(router, 
testCase.cfg))
                        router.Use(func(c *gin.Context) {
                                c.Request = c.Request.WithContext(
                                        
logging.ContextWithLogger(c.Request.Context(), logger),
                                )
                                c.Next()
                        })
-                       router.Use(LoggingMiddleware())
+                       router.Use(LoggingMiddleware(testCase.cfg))
                        router.Use(s.handleError)
                        router.GET("/", testCase.handler)
 
                        w := httptest.NewRecorder()
-                       router.ServeHTTP(w, httptest.NewRequest(http.MethodGet, 
"/", nil))
+                       req := httptest.NewRequest(http.MethodGet, "/", nil)
+                       for k, v := range testCase.headers {
+                               req.Header.Set(k, v)
+                       }
+                       router.ServeHTTP(w, req)
 
                        require.Equal(t, testCase.expectedStatus, w.Code)
                        testCase.assertions(t, recorded.All())
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kargo-cli-1.12.1/pkg/server/rest_router.go 
new/kargo-cli-1.12.2/pkg/server/rest_router.go
--- old/kargo-cli-1.12.1/pkg/server/rest_router.go      2026-10-02 
22:45:24.000000000 +0200
+++ new/kargo-cli-1.12.2/pkg/server/rest_router.go      2026-10-06 
20:24:02.000000000 +0200
@@ -52,18 +52,11 @@
 // @in header
 // @name Authorization
 // @description Bearer token authentication. Obtain token via OIDC/PKCE flow 
with your identity provider.
-func (s *server) setupRESTRouter(ctx context.Context) *gin.Engine {
-       // Unconditionally set Gin to "release mode" (as opposed to the default 
of
-       // "debug mode"). This suppresses Gin-related log noise that we don't 
want,
-       // even at development time.
-       //
-       // Note: The mode is package-level state, so the following statements 
reaches
-       // beyond this function. This is tolerable because the engine built 
below is
-       // the only one the server ever builds. Were we ever to build more, we 
would,
-       // in all likelihood, want them all to be in release mode as well.
-       gin.SetMode(gin.ReleaseMode)
-
+func (s *server) setupRESTRouter(ctx context.Context) (*gin.Engine, error) {
        router := gin.New()
+       if err := ConfigureEngine(router, s.cfg); err != nil {
+               return nil, err
+       }
 
        // Middleware nests, with each layer registered here wrapping the ones
        // registered after it. Each of the layers below does its work on the 
way back
@@ -88,7 +81,7 @@
        // have to write its own response. Authentication is innermost of the 
four so
        // that its rejections are answered by the error handling middleware, 
and so
        // that a panic within it is recovered too.
-       router.Use(LoggingMiddleware())
+       router.Use(LoggingMiddleware(s.cfg))
        router.Use(s.handleError)
        router.Use(recoveryMiddleware())
        if s.cfg.AdminConfig != nil || s.cfg.OIDCConfig != nil {
@@ -336,7 +329,7 @@
                }
        }
 
-       return router
+       return router, nil
 }
 
 // errorResponse is the body of every error response the REST API sends.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kargo-cli-1.12.1/pkg/server/rest_test.go 
new/kargo-cli-1.12.2/pkg/server/rest_test.go
--- old/kargo-cli-1.12.1/pkg/server/rest_test.go        2026-10-02 
22:45:24.000000000 +0200
+++ new/kargo-cli-1.12.2/pkg/server/rest_test.go        2026-10-06 
20:24:02.000000000 +0200
@@ -182,7 +182,8 @@
                        for key, value := range testCase.headers {
                                req.Header.Set(key, value)
                        }
-                       router := s.setupRESTRouter(t.Context())
+                       router, err := s.setupRESTRouter(t.Context())
+                       require.NoError(t, err)
 
                        router.ServeHTTP(w, req)
 
@@ -289,7 +290,8 @@
                                }()
                        }
 
-                       router := s.setupRESTRouter(t.Context())
+                       router, err := s.setupRESTRouter(t.Context())
+                       require.NoError(t, err)
                        router.ServeHTTP(w, req)
 
                        testCase.assertions(t, w, internalClient)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kargo-cli-1.12.1/pkg/server/server.go 
new/kargo-cli-1.12.2/pkg/server/server.go
--- old/kargo-cli-1.12.1/pkg/server/server.go   2026-10-02 22:45:24.000000000 
+0200
+++ new/kargo-cli-1.12.2/pkg/server/server.go   2026-10-06 20:24:02.000000000 
+0200
@@ -200,7 +200,10 @@
        }
 
        // Add Gin REST router
-       ginRouter := s.setupRESTRouter(ctx)
+       ginRouter, err := s.setupRESTRouter(ctx)
+       if err != nil {
+               return fmt.Errorf("error setting up REST router: %w", err)
+       }
        mux.Handle("/v1beta1/", ginRouter)
 
        var dashboardFS fs.FS

++++++ kargo-cli.obsinfo ++++++
--- /var/tmp/diff_new_pack.QotTKH/_old  2026-10-07 19:34:33.439539780 +0200
+++ /var/tmp/diff_new_pack.QotTKH/_new  2026-10-07 19:34:33.446540075 +0200
@@ -1,5 +1,5 @@
 name: kargo-cli
-version: 1.12.1
-mtime: 1790973924
-commit: c2316a2ffd24e9263783099b5238c3c4fda35707
+version: 1.12.2
+mtime: 1791311042
+commit: 2b5f3a2f12c1eb6156253ba71d44818d4262c865
 

++++++ vendor.tar.gz ++++++
/work/SRC/openSUSE:Factory/kargo-cli/vendor.tar.gz 
/work/SRC/openSUSE:Factory/.kargo-cli.new.3698736/vendor.tar.gz differ: char 
132, line 2

Reply via email to