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 @@  +## 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. + + + +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. + + + +### 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. + + + +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: + + + +- **<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
