Skip to content

hook-reverse-proxy: First implementation - #5345

Merged
openshift-merge-bot[bot] merged 2 commits into
openshift:mainfrom
danilo-gemoli:feat/hook-reverse-proxy/first-implementation
Jul 31, 2026
Merged

hook-reverse-proxy: First implementation#5345
openshift-merge-bot[bot] merged 2 commits into
openshift:mainfrom
danilo-gemoli:feat/hook-reverse-proxy/first-implementation

Conversation

@danilo-gemoli

@danilo-gemoli danilo-gemoli commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

This is an ad-hoc reverse proxy that will sit in front of hook, created to ease the migration of Prow from app.ci to core-ci.

Since shutting down Prow on app.ci, recreating it on core-ci and migrate everything is too risk, we are going to follow this plan:

  1. Deploy Prow on core-ci.
  2. Deploy hook-reverse-proxy on app.ci.
  3. Incrementally migrate repos and orgs by leveraging hook-reverse-proxy.
    • Fix any error along the way

The reverse proxy reads a config file in order to match an endpoint to dispatch the next GitHub event to:

routes:
- target: http://hook.ci.svc.cluster.local:8888/hook
  matches:
  - org: openshift
    repos:
    - ci-tools
default_route:
  target:  https://hook-ci.apps.master.ci.devcluster.openshift.com/hook

GitHub events generated by openshift/ci-tools will be forwarded to hook on core-ci, everything else to standard hook on app.ci. In this way we can gradually add new orgs/repos and assess the result.

Summary

  • Adds an ad-hoc reverse proxy for Hook to support the Prow migration from app.ci to core-ci.
  • Routes GitHub webhook events by organization and repository. Configured repositories can use core-ci, while unmatched events use the default app.ci Hook endpoint.
  • Adds YAML configuration validation and hot reload support.
  • Adds request validation, route matching, wildcard support, URL rewriting, health readiness, structured logging, and graceful shutdown.
  • Adds a container image definition for deployment.

@openshift-merge-bot

Copy link
Copy Markdown
Contributor

Pipeline controller notification
This repo is configured to use the pipeline controller. Second-stage tests will be triggered either automatically or after lgtm label is added, depending on the repository configuration. The pipeline controller will automatically detect which contexts are required and will utilize /test Prow commands to trigger the second stage.

For optional jobs, comment /test ? to see a list of all defined jobs. To trigger manually all jobs from second stage use /pipeline required command.

This repository is configured in: automatic mode

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds a configurable GitHub webhook reverse proxy. The service validates and routes webhook requests, reloads YAML configuration, exposes readiness health, supports graceful shutdown, and includes a UBI9-based container image.

Changes

Reverse Proxy Service

Layer / File(s) Summary
Configuration and reload lifecycle
cmd/hook-reverse-proxy/config.go, cmd/hook-reverse-proxy/router.go
Adds URL, route, and match configuration types. Validates routes, loads YAML configuration, synchronizes access, and watches for filesystem changes.
Webhook validation and route selection
cmd/hook-reverse-proxy/router.go, cmd/hook-reverse-proxy/router_test.go
Validates GitHub webhook requests, selects exact or wildcard organization and repository routes, rewrites proxy destinations, and tests fallback and invalid-request behavior.
Service startup and container packaging
cmd/hook-reverse-proxy/main.go, images/hook-reverse-proxy/Dockerfile
Adds flag parsing, JSON logging, HTTP serving, readiness health, interrupt handling, graceful shutdown, and a UBI9 container entrypoint.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Suggested reviewers: pruan-rht, smg247


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (2 errors, 3 warnings)

Check name Status Explanation Resolution
Container-Privileges ❌ Error The new UBI9 image has no USER directive, so the proxy runs as root by default; the PR provides no root justification, and the app only listens on port 8888. Run the image as a dedicated non-root UID, for example with a USER directive, and verify file and port permissions.
No-Sensitive-Data-In-Logs ❌ Error router.go logs route.Target as target-url; configured targets can expose internal hostnames such as hook.ci.svc.cluster.local and URL query data. Do not log complete target URLs. Log a sanitized route identifier or hostname allowlist, and redact credentials and query parameters.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Go Error Handling ⚠️ Warning config.go returns url.Parse and flag.Parse errors without %w context, and router.go dereferences conf.DefaultRoute.Target.URL without guarding the pointer chain. Wrap parse errors with fmt.Errorf("...: %w", err). Validate URL.URL and guard conf, DefaultRoute, and Target before dereferencing in rewrite and route matching.
Test Coverage For New Features ⚠️ Warning Only router_test.go tests rewrite routing; new URL.UnmarshalText, Config.validate, watchConfig, loadConfig, newRouter, and gatherOptions have no corresponding unit tests. Add table-driven tests for URL parsing, config validation/loading, option parsing, router construction, and watcher reload/cancellation; retain routing regression coverage.
✅ Passed checks (12 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the new hook-reverse-proxy implementation, which is the primary change in the pull request.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Stable And Deterministic Test Names ✅ Passed No Ginkgo imports or Ginkgo title calls exist; the added tests use static table-driven t.Run names such as "Match org wildcard".
Test Structure And Quality ✅ Passed The added test uses Go testing, not Ginkgo; it has isolated table subtests, no cluster resources or waits, and includes contextual failure messages.
Microshift Test Compatibility ✅ Passed The changed test is a standard Go TestRewrite unit test, not a Ginkgo e2e test, and it references no MicroShift-unavailable OpenShift APIs or resources.
Single Node Openshift (Sno) Test Compatibility ✅ Passed The only added test is Go's testing-based TestRewrite; it adds no Ginkgo e2e tests and makes no multi-node or HA assumptions.
Topology-Aware Scheduling Compatibility ✅ Passed The change adds application code and tests only; no deployment manifests, controllers, or scheduling constraints were added or modified. The Dockerfile is unchanged.
Ote Binary Stdout Contract ✅ Passed The PR adds a reverse-proxy service, not an OTE test binary; main has no stdout writes, klog, or standard-log output, and uses logrus logging.
Ipv6 And Disconnected Network Test Compatibility ✅ Passed The PR adds a standard Go unit test, not a Ginkgo e2e test. It uses httptest.NewRequest and URL rewriting only, with no IPv4 literals or network calls.
No-Weak-Crypto ✅ Passed The added code has no crypto imports, weak-algorithm identifiers, custom cryptography, or secret/token comparisons; its equality checks cover routing and request metadata only.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@openshift-ci
openshift-ci Bot requested review from pruan-rht and smg247 July 29, 2026 20:52
@openshift-ci openshift-ci Bot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Jul 29, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
images/hook-reverse-proxy/Dockerfile (1)

1-5: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Missing non-root USER, ADD instead of COPY, and no HEALTHCHECK.

Three separate guideline violations here:

  • No USER directive — the image runs as root by default.
  • ADD used for a plain local file copy; COPY is the correct, less "magic" instruction.
  • No HEALTHCHECK, even though the binary exposes a readiness endpoint via pjutil.NewHealthOnPort.
🔒 Proposed fix
 FROM registry.access.redhat.com/ubi9/ubi-minimal:latest
 
-ADD hook-reverse-proxy /usr/bin/hook-reverse-proxy
+COPY hook-reverse-proxy /usr/bin/hook-reverse-proxy
+USER 65534:65534
 ENTRYPOINT ["/usr/bin/hook-reverse-proxy"]
As per path instructions, "USER non-root; never run as root" and "HEALTHCHECK defined."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@images/hook-reverse-proxy/Dockerfile` around lines 1 - 5, Update the
hook-reverse-proxy Dockerfile to replace ADD with COPY, create or select a
non-root user and set it with USER before ENTRYPOINT, and add a HEALTHCHECK
targeting the readiness endpoint exposed by pjutil.NewHealthOnPort. Preserve the
existing binary path and startup command.

Sources: Path instructions, Linters/SAST tools

🧹 Nitpick comments (3)
cmd/hook-reverse-proxy/main.go (3)

142-186: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Loop variable r shadows the *router receiver.

Inside matchRoute, for _, r := range m.Repos at line 171 shadows the method receiver r *router in scope for the rest of the function. It's harmless today (the inner block never touches r.config), but it's a readability trap for future edits — someone adding router-state access inside that inner loop would silently reference the wrong r.

🏷️ Proposed rename
-			for _, r := range m.Repos {
-				if r == "*" {
+			for _, repoPattern := range m.Repos {
+				if repoPattern == "*" {
 					log.Infof("Match found: match org %q and * repo wildcard", org)
 					return route.Target.URL
 				}
 
-				if r == repo {
+				if repoPattern == repo {
 					log.Infof("Match found: match org %q and repo %q", org, repo)
 					return route.Target.URL
 				}
 			}
As per coding guidelines, "Use good naming conventions that are long enough to communicate fully without being hard to read."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/hook-reverse-proxy/main.go` around lines 142 - 186, Rename the inner loop
variable in matchRoute that iterates over m.Repos so it no longer shadows the
*router receiver r. Update its comparison with repo accordingly, preserving the
existing wildcard and exact-match behavior.

Source: Coding guidelines


85-97: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Nil-deref risk in fallback if DefaultRoute is unset.

Line 91 dereferences r.config.DefaultRoute.Target.URL without a nil check. Today this is safe only because Config.validate() (line 48-64) is always run in loadConfig before a router is constructed in main(). But newRouter itself accepts any *Config, so any future caller that skips loadConfig/validate() (e.g. programmatic construction, a different entrypoint) will panic here. Worth guarding defensively given this runs per-request.

As per path instructions, "check for nil before dereferencing pointers."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/hook-reverse-proxy/main.go` around lines 85 - 97, The deferred fallback
in the router response handling can panic when DefaultRoute or its Target is
nil. Update the targetURL fallback in the defer block to check each pointer
before accessing URL, while preserving the existing default-route behavior when
the nested values are present.

Source: Path instructions


20-21: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate import of the same package under two aliases.

Lines 20-21 import sigs.k8s.io/prow/pkg/flagutil twice — once unaliased (flagutil, used at line 210) and once aliased (prowflagutil, used at line 200). This compiles, but is a known code smell flagged by linters like go-critic's dupImport/revive's duplicated-imports and invites confusion about whether these are different packages.

🧹 Proposed fix
-	"sigs.k8s.io/prow/pkg/flagutil"
-	prowflagutil "sigs.k8s.io/prow/pkg/flagutil"
+	"sigs.k8s.io/prow/pkg/flagutil"

And then replace prowflagutil.InstrumentationOptions at line 200 with flagutil.InstrumentationOptions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/hook-reverse-proxy/main.go` around lines 20 - 21, Remove the duplicate
aliased import of sigs.k8s.io/prow/pkg/flagutil and use the existing flagutil
import consistently. Update the prowflagutil.InstrumentationOptions reference in
the surrounding initialization code to flagutil.InstrumentationOptions, while
preserving the existing flagutil usage elsewhere.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/hook-reverse-proxy/main.go`:
- Around line 263-268: Update the http.Server initialization in the
reverse-proxy setup to configure finite ReadHeaderTimeout, ReadTimeout,
WriteTimeout, and IdleTimeout values appropriate for GitHub webhook traffic.
Keep the existing Addr and ReverseProxy handler unchanged while ensuring slow or
idle clients cannot hold connections indefinitely.

---

Outside diff comments:
In `@images/hook-reverse-proxy/Dockerfile`:
- Around line 1-5: Update the hook-reverse-proxy Dockerfile to replace ADD with
COPY, create or select a non-root user and set it with USER before ENTRYPOINT,
and add a HEALTHCHECK targeting the readiness endpoint exposed by
pjutil.NewHealthOnPort. Preserve the existing binary path and startup command.

---

Nitpick comments:
In `@cmd/hook-reverse-proxy/main.go`:
- Around line 142-186: Rename the inner loop variable in matchRoute that
iterates over m.Repos so it no longer shadows the *router receiver r. Update its
comparison with repo accordingly, preserving the existing wildcard and
exact-match behavior.
- Around line 85-97: The deferred fallback in the router response handling can
panic when DefaultRoute or its Target is nil. Update the targetURL fallback in
the defer block to check each pointer before accessing URL, while preserving the
existing default-route behavior when the nested values are present.
- Around line 20-21: Remove the duplicate aliased import of
sigs.k8s.io/prow/pkg/flagutil and use the existing flagutil import consistently.
Update the prowflagutil.InstrumentationOptions reference in the surrounding
initialization code to flagutil.InstrumentationOptions, while preserving the
existing flagutil usage elsewhere.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited)

Review profile: CHILL

Plan: Enterprise

Run ID: 28ccc0f2-1cb0-4c05-b97a-47947d346bdd

📥 Commits

Reviewing files that changed from the base of the PR and between 7fa17c5 and 6208ea0.

📒 Files selected for processing (3)
  • cmd/hook-reverse-proxy/main.go
  • cmd/hook-reverse-proxy/main_test.go
  • images/hook-reverse-proxy/Dockerfile
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • openshift/release (manual)
  • openshift/ci-docs (manual)
  • openshift/release-controller (manual)
  • openshift/ci-chat-bot (manual)

Comment thread cmd/hook-reverse-proxy/main.go
@danilo-gemoli
danilo-gemoli force-pushed the feat/hook-reverse-proxy/first-implementation branch from 6208ea0 to e8643c7 Compare July 30, 2026 12:08

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
cmd/hook-reverse-proxy/main.go (3)

172-172: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Loop variable r shadows the method receiver r *router.

Harmless today since the receiver isn't referenced inside the inner loop, but it's a latent footgun if someone later adds router-scoped logic there. Rename to something like repo.

As per coding guidelines, use "good naming conventions that are long enough to communicate fully without being hard to read."

♻️ Proposed fix
-			for _, r := range m.Repos {
-				if r == "*" {
+			for _, repo := range m.Repos {
+				if repo == "*" {
 					log.Infof("Match found: match org %q and * repo wildcard", org)
 					return route.Target.URL
 				}

-				if r == repo {
+				if repo == event.Repo.Name {
 					log.Infof("Match found: match org %q and repo %q", org, repo)
 					return route.Target.URL
 				}
 			}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/hook-reverse-proxy/main.go` at line 172, Rename the inner loop variable
`r` in the repository iteration over `m.Repos` to `repo` or another descriptive
name, and update all references within that loop while preserving the method
receiver `r *router` unchanged.

Source: Coding guidelines


44-75: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add doc comments to exported config types.

Config, Route, and Match are exported types defining the on-disk YAML schema but have no doc comments explaining their fields/semantics (e.g. what "*" means for Org/Repos).

As per coding guidelines, "Go documentation on Classes/Functions/Fields should be written properly."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/hook-reverse-proxy/main.go` around lines 44 - 75, Add Go doc comments for
the exported Config, Route, and Match types, describing their YAML schema roles
and field semantics, including that "*" is supported as a wildcard for Match.Org
and Match.Repos. Keep validation behavior unchanged.

Source: Coding guidelines


21-22: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate import of the same package under two aliases.

Lines 21-22 import sigs.k8s.io/prow/pkg/flagutil twice, once unaliased and once as prowflagutil. flagutil.OptionGroup (Line 211) and prowflagutil.InstrumentationOptions (Line 201) refer to the same package. Consolidate to a single import name.

♻️ Proposed fix
-	"github.com/sirupsen/logrus"
-	"gopkg.in/yaml.v3"
-
-	aggerrs "k8s.io/apimachinery/pkg/util/errors"
-	"sigs.k8s.io/prow/pkg/flagutil"
-	prowflagutil "sigs.k8s.io/prow/pkg/flagutil"
+	"github.com/sirupsen/logrus"
+	"gopkg.in/yaml.v3"
+
+	aggerrs "k8s.io/apimachinery/pkg/util/errors"
+	"sigs.k8s.io/prow/pkg/flagutil"

Then use flagutil.InstrumentationOptions at Line 201 instead of prowflagutil.InstrumentationOptions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/hook-reverse-proxy/main.go` around lines 21 - 22, Remove the duplicate
unaliased/aliased imports of sigs.k8s.io/prow/pkg/flagutil, retain the single
flagutil import, and update the InstrumentationOptions reference to use flagutil
while preserving the existing flagutil.OptionGroup usage.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/hook-reverse-proxy/main.go`:
- Around line 49-65: Update Config.validate to reject unusable target URLs, not
just nil pointers: validate both c.DefaultRoute.Target and each route.Target for
the required parsed URL fields (including non-empty scheme and host), and append
the existing aggregate validation errors with context identifying the affected
route. Preserve the current nil-target checks and return through
aggerrs.NewAggregate.
- Around line 127-132: Update the request-body handling around io.ReadAll in the
webhook handler to read through io.LimitReader with a 25 MB limit, then detect
when the payload exceeds that limit and reject it before processing. Preserve
the existing read-error logging and return behavior for actual read failures.

In `@images/hook-reverse-proxy/Dockerfile`:
- Around line 1-4: Add a dedicated non-root user to the Dockerfile after the
hook-reverse-proxy binary is copied, then set the image’s USER to that account
before ENTRYPOINT so the proxy runs without root privileges.

---

Nitpick comments:
In `@cmd/hook-reverse-proxy/main.go`:
- Line 172: Rename the inner loop variable `r` in the repository iteration over
`m.Repos` to `repo` or another descriptive name, and update all references
within that loop while preserving the method receiver `r *router` unchanged.
- Around line 44-75: Add Go doc comments for the exported Config, Route, and
Match types, describing their YAML schema roles and field semantics, including
that "*" is supported as a wildcard for Match.Org and Match.Repos. Keep
validation behavior unchanged.
- Around line 21-22: Remove the duplicate unaliased/aliased imports of
sigs.k8s.io/prow/pkg/flagutil, retain the single flagutil import, and update the
InstrumentationOptions reference to use flagutil while preserving the existing
flagutil.OptionGroup usage.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited)

Review profile: CHILL

Plan: Enterprise

Run ID: 51fd7b85-8843-4b9f-86e1-a4bc38603c15

📥 Commits

Reviewing files that changed from the base of the PR and between 6208ea0 and e8643c7.

📒 Files selected for processing (3)
  • cmd/hook-reverse-proxy/main.go
  • cmd/hook-reverse-proxy/main_test.go
  • images/hook-reverse-proxy/Dockerfile
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • openshift/release (manual)
  • openshift/ci-docs (manual)
  • openshift/release-controller (manual)
  • openshift/ci-chat-bot (manual)

Comment thread cmd/hook-reverse-proxy/main.go Outdated
Comment on lines +49 to +65
func (c *Config) validate() error {
var errs []error

if c.DefaultRoute == nil {
errs = append(errs, errors.New("default route is not defined"))
} else if c.DefaultRoute.Target == nil {
errs = append(errs, errors.New("default route target URL is not defined"))
}

for i, route := range c.Routes {
if route.Target == nil {
errs = append(errs, fmt.Errorf("route[%d]: target is nil", i))
}
}

return aggerrs.NewAggregate(errs)
}

@coderabbitai coderabbitai Bot Jul 30, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

validate() doesn't catch empty/invalid target URLs.

It only checks Target == nil, not that the parsed URL is actually usable. A config with target: "" produces a non-nil *URL wrapping an empty url.URL (no Host/Scheme), which passes validation but will fail silently at proxy time (outbound request with empty Host).

🛡️ Proposed fix
 	if c.DefaultRoute == nil {
 		errs = append(errs, errors.New("default route is not defined"))
 	} else if c.DefaultRoute.Target == nil {
 		errs = append(errs, errors.New("default route target URL is not defined"))
+	} else if c.DefaultRoute.Target.Host == "" {
+		errs = append(errs, errors.New("default route target URL has no host"))
 	}

 	for i, route := range c.Routes {
 		if route.Target == nil {
 			errs = append(errs, fmt.Errorf("route[%d]: target is nil", i))
+		} else if route.Target.Host == "" {
+			errs = append(errs, fmt.Errorf("route[%d]: target URL has no host", i))
 		}
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (c *Config) validate() error {
var errs []error
if c.DefaultRoute == nil {
errs = append(errs, errors.New("default route is not defined"))
} else if c.DefaultRoute.Target == nil {
errs = append(errs, errors.New("default route target URL is not defined"))
}
for i, route := range c.Routes {
if route.Target == nil {
errs = append(errs, fmt.Errorf("route[%d]: target is nil", i))
}
}
return aggerrs.NewAggregate(errs)
}
func (c *Config) validate() error {
var errs []error
if c.DefaultRoute == nil {
errs = append(errs, errors.New("default route is not defined"))
} else if c.DefaultRoute.Target == nil {
errs = append(errs, errors.New("default route target URL is not defined"))
} else if c.DefaultRoute.Target.Host == "" {
errs = append(errs, errors.New("default route target URL has no host"))
}
for i, route := range c.Routes {
if route.Target == nil {
errs = append(errs, fmt.Errorf("route[%d]: target is nil", i))
} else if route.Target.Host == "" {
errs = append(errs, fmt.Errorf("route[%d]: target URL has no host", i))
}
}
return aggerrs.NewAggregate(errs)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/hook-reverse-proxy/main.go` around lines 49 - 65, Update Config.validate
to reject unusable target URLs, not just nil pointers: validate both
c.DefaultRoute.Target and each route.Target for the required parsed URL fields
(including non-empty scheme and host), and append the existing aggregate
validation errors with context identifying the affected route. Preserve the
current nil-target checks and return through aggerrs.NewAggregate.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the URL is not valid then the custom unmarshal function would fail:

// UnmarshalText implements [encoding.UnmarshalText].
func (u *URL) UnmarshalText(text []byte) error {
	parsed, err := url.Parse(string(text))
	if err != nil {
		return err
	}
	u.URL = parsed
	return nil
}

I think that makes the validation code you are proposing pointless.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@danilo-gemoli, url.Parse does not require an absolute URL.

For example, url.Parse("") and url.Parse("/hook") return no error. Both results have an empty Host. A relative URL is valid URL syntax, but it is not a usable upstream target for this reverse proxy.

UnmarshalText should continue to reject malformed URL syntax. Config.validate() should also reject parsed targets that lack the required Scheme and Host.

The finding is therefore still valid. I will update the wording to make this distinction clear.

You are interacting with an AI system.

Comment thread cmd/hook-reverse-proxy/main.go Outdated
Comment thread images/hook-reverse-proxy/Dockerfile

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
cmd/hook-reverse-proxy/router.go (1)

90-134: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Loop variable r shadows the method receiver r *router.

matchRoute is a method on r *router. Inside the nested loop at Line 119, for _, r := range m.Repos reintroduces r as a string, shadowing the receiver for the rest of that inner loop. No bug results today because the receiver is not referenced inside the loop, but this is a latent trap: a future edit that tries to call a router method (r.something) inside this block will silently bind to the wrong r or fail to compile with a confusing error.

Rename the loop variable to avoid the collision.

♻️ Proposed fix
-			for _, r := range m.Repos {
-				if r == "*" {
+			for _, repoPattern := range m.Repos {
+				if repoPattern == "*" {
 					log.Infof("Match found: match org %q and * repo wildcard", org)
 					return route.Target.URL
 				}

-				if r == repo {
+				if repoPattern == repo {
 					log.Infof("Match found: match org %q and repo %q", org, repo)
 					return route.Target.URL
 				}
 			}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/hook-reverse-proxy/router.go` around lines 90 - 134, In the matchRoute
method, rename the nested m.Repos loop variable r to a non-conflicting name and
update its comparisons accordingly, preserving the existing wildcard and
repository-match behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/hook-reverse-proxy/config.go`:
- Around line 40-53: Update validate() so every non-nil target URL must also be
an absolute URL with a non-empty Host, rejecting empty and relative targets for
both DefaultRoute.Target and entries in c.Routes. Preserve the existing
nil-target aggregate errors and route-specific error context.
- Around line 65-110: Update the watcher producer returned by
GetWatcher/startWatching so every send to EventCh and ErrCh is
cancellation-aware, selecting between the channel send and ctx.Done(). Ensure
cancellation allows the producer to exit and w.Close() to run even when
watchConfig’s consumer goroutine has already stopped; preserve the existing
event and error handling behavior while avoiding blocked sends.

---

Nitpick comments:
In `@cmd/hook-reverse-proxy/router.go`:
- Around line 90-134: In the matchRoute method, rename the nested m.Repos loop
variable r to a non-conflicting name and update its comparisons accordingly,
preserving the existing wildcard and repository-match behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited)

Review profile: CHILL

Plan: Enterprise

Run ID: 81fbbfcc-5051-47ac-906e-343149fbef8b

📥 Commits

Reviewing files that changed from the base of the PR and between e8643c7 and 441a71a.

📒 Files selected for processing (4)
  • cmd/hook-reverse-proxy/config.go
  • cmd/hook-reverse-proxy/main.go
  • cmd/hook-reverse-proxy/router.go
  • cmd/hook-reverse-proxy/router_test.go
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • openshift/release (manual)
  • openshift/ci-docs (manual)
  • openshift/release-controller (manual)
  • openshift/ci-chat-bot (manual)

Comment on lines +40 to +53
if c.DefaultRoute == nil {
errs = append(errs, errors.New("default route is not defined"))
} else if c.DefaultRoute.Target == nil {
errs = append(errs, errors.New("default route target URL is not defined"))
}

for i, route := range c.Routes {
if route.Target == nil {
errs = append(errs, fmt.Errorf("route[%d]: target is nil", i))
}
}

return aggerrs.NewAggregate(errs)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

validate() accepts syntactically valid but unusable target URLs.

validate() only checks Target == nil. A config with target: "" or target: "/hook" produces a non-nil *URL since url.Parse accepts relative and empty URLs without error. Host stays empty, and the check at Line 42-44 and Line 47-49 passes, but the reverse proxy fails silently at request time because pr.SetURL/pr.Out.URL end up pointing at an unusable target.

This concern was already raised on a previous commit of this PR, when this validation logic lived in main.go. The discussion concluded the finding is still valid because url.Parse accepts relative URLs; it has not yet been fixed and the same logic is now in config.go.

🛡️ Proposed fix
 	if c.DefaultRoute == nil {
 		errs = append(errs, errors.New("default route is not defined"))
 	} else if c.DefaultRoute.Target == nil {
 		errs = append(errs, errors.New("default route target URL is not defined"))
+	} else if c.DefaultRoute.Target.Host == "" {
+		errs = append(errs, errors.New("default route target URL has no host"))
 	}

 	for i, route := range c.Routes {
 		if route.Target == nil {
 			errs = append(errs, fmt.Errorf("route[%d]: target is nil", i))
+		} else if route.Target.Host == "" {
+			errs = append(errs, fmt.Errorf("route[%d]: target URL has no host", i))
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/hook-reverse-proxy/config.go` around lines 40 - 53, Update validate() so
every non-nil target URL must also be an absolute URL with a non-empty Host,
rejecting empty and relative targets for both DefaultRoute.Target and entries in
c.Routes. Preserve the existing nil-target aggregate errors and route-specific
error context.

Comment on lines +65 to +110
func watchConfig(ctx context.Context, log *logrus.Entry, path string, onNewConfig func() error) error {
log = log.WithField("config_path", path)

events := make(chan fsnotify.Event)
errs := make(chan error)
w := agents.UniversalSymlinkWatcher{
EventCh: events,
ErrCh: errs,
WatchPath: path,
}

startWatching, err := w.GetWatcher()
if err != nil {
return fmt.Errorf("get watcher: %w", err)
}

go func() {
for {
select {
case _, ok := <-events:
if !ok {
log.Warn("Watch config: events channel closed")
return
}
if err := onNewConfig(); err != nil {
log.WithError(err).Error("Failed to reload the configuration")
} else {
log.Info("Configuration reloaded")
}
case err, ok := <-errs:
if !ok {
log.Warn("Watch config: errors channel closed")
return
}
log.WithError(err).Error("Failed to watch the configuration file")
case <-ctx.Done():
log.WithError(context.Cause(ctx)).Info("Configuration watcher stopped")
return
}
}
}()

go startWatching(ctx)

return nil
}

@coderabbitai coderabbitai Bot Jul 31, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
fd -t f . pkg/load/agents | xargs rg -n -C 15 'UniversalSymlinkWatcher|GetWatcher'

Repository: openshift/ci-tools

Length of output: 13186


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '107,190p' pkg/load/agents/utils.go
printf '\n-- watchConfig call sites --\n'
rg -n -C 8 'watchConfig\(' cmd pkg
printf '\n-- watcher tests --\n'
rg -n -C 12 'GetWatcher|UniversalSymlinkWatcher|fsnotify watcher|watch.*config' pkg/load/agents cmd/hook-reverse-proxy

Repository: openshift/ci-tools

Length of output: 22378


🏁 Script executed:

#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
p = Path("pkg/load/agents/utils.go")
lines = p.read_text().splitlines()
for i in range(129, 175):
    print(f"{i+1:4}: {lines[i]}")
PY

Repository: openshift/ci-tools

Length of output: 1157


🏁 Script executed:

#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
source = Path("pkg/load/agents/utils.go").read_text()
checks = {
    "context cancellation closes fsnotify watcher": "case <-ctx.Done():" in source and "w.Close()" in source,
    "event send is cancellation-aware": 'select {\n\t\t\t\t\tcase u.EventCh <- event:' in source,
    "close-error send is cancellation-aware": 'select {\n\t\t\t\t\tcase u.ErrCh <- fmt.Errorf("failed to close' in source,
    "fsnotify-error send is cancellation-aware": 'select {\n\t\t\t\t\tcase u.ErrCh <- fmt.Errorf("received fsnotify error' in source,
}
for name, result in checks.items():
    print(f"{name}: {'yes' if result else 'no'}")
PY

Repository: openshift/ci-tools

Length of output: 332


Make watcher output sends cancellation-aware.

GetWatcher handles ctx.Done() and calls w.Close(), but startWatching can block on the unguarded u.EventCh and u.ErrCh sends. If watchConfig exits first, the watcher goroutine can remain blocked, and an event send can prevent w.Close() from running. Guard each send with ctx.Done() or coordinate producer shutdown before consumer shutdown.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/hook-reverse-proxy/config.go` around lines 65 - 110, Update the watcher
producer returned by GetWatcher/startWatching so every send to EventCh and ErrCh
is cancellation-aware, selecting between the channel send and ctx.Done(). Ensure
cancellation allows the producer to exit and w.Close() to run even when
watchConfig’s consumer goroutine has already stopped; preserve the existing
event and error handling behavior while avoiding blocked sends.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Propose a fix

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '-- UniversalSymlinkWatcher definitions --'
rg -n -C 20 '\btype UniversalSymlinkWatcher\b|\bfunc \(u UniversalSymlinkWatcher\) GetWatcher\b' pkg/load/agents

printf '%s\n' '-- watcher call sites --'
rg -n -C 6 '\.GetWatcher\(\)|UniversalSymlinkWatcher' --glob '*.go'

Length of output: 2892


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '120,205p' pkg/load/agents/utils.go

printf '%s\n' '-- call sites --'
rg -n -C 6 '\bGetWatcher\s*\(' --glob '*.go' . || true

Length of output: 5852


@danilo-gemoli, the fix belongs in the shared UniversalSymlinkWatcher implementation. This protects the reverse proxy and the existing watcher consumers.

  • Update every u.EventCh and u.ErrCh send in pkg/load/agents/utils.go to select on ctx.Done().
  • Return from the watcher function when cancellation wins.
  • Add a focused regression test for cancellation with unread output channels.

  • 📌 Create a pull request with these changes

You are interacting with an AI system.

@danilo-gemoli

Copy link
Copy Markdown
Contributor Author

/override ci/prow/integration

@deepsm007

deepsm007 commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

/lgtm

@openshift-merge-bot

Copy link
Copy Markdown
Contributor

Pipeline controller notification

No second-stage tests were triggered for this PR.

This can happen when:

  • The changed files don't match any pipeline_run_if_changed patterns
  • All files match pipeline_skip_if_only_changed patterns
  • No pipeline-controlled jobs are defined for the main branch

Use /test ? to see all available tests.

@openshift-ci

openshift-ci Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

@danilo-gemoli: Overrode contexts on behalf of danilo-gemoli: ci/prow/integration

Details

In response to this:

/override ci/prow/integration

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

@openshift-ci openshift-ci Bot added the lgtm Indicates that a PR is ready to be merged. label Jul 31, 2026
@openshift-ci

openshift-ci Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: danilo-gemoli, deepsm007

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:
  • OWNERS [danilo-gemoli,deepsm007]

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@openshift-ci

openshift-ci Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

@deepsm007: Overrode contexts on behalf of deepsm007: ci/prow/integration

Details

In response to this:

/lgtm
/override ci/prow/integration
unrelated failures

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

@openshift-ci

openshift-ci Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

@danilo-gemoli: all tests passed!

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

@openshift-merge-bot
openshift-merge-bot Bot merged commit 19f5c58 into openshift:main Jul 31, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Indicates a PR has been approved by an approver from all required OWNERS files. lgtm Indicates that a PR is ready to be merged.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants