Skip to content

Optimize roxagent binary size by 68% through package splitting#19804

Draft
janisz wants to merge 1 commit intomasterfrom
optimize-roxagent-binary-size
Draft

Optimize roxagent binary size by 68% through package splitting#19804
janisz wants to merge 1 commit intomasterfrom
optimize-roxagent-binary-size

Conversation

@janisz
Copy link
Copy Markdown
Contributor

@janisz janisz commented Apr 2, 2026

Problem:

  • roxagent binary was 98 MB despite containing only ~1,300 lines of code
  • Pulled in entire StackRox monorepo dependency graph (1,972 packages)
  • Included 408 k8s.io packages, 96 cloud provider SDKs, and registry clients
  • All unnecessary for VM vulnerability scanning

Solution (Package Splitting - Option 3):
Created compliance/node/vm/ package with minimal dependencies:

  • VM-specific indexer bypassing pkg/registries → pkg/cloudproviders → k8s.io chain
  • Lightweight protobuf conversion replacing heavy pkg/scannerv4/mappers
  • Direct stdlib usage instead of pkg/httputil/proxy, pkg/logging, pkg/mtls

Results:

  • Binary size: 98 MB → 31 MB (-67 MB, 68.4% reduction) (on release build and 45 on develop)
  • Dependencies: 1,972 → 499 packages (74.7% reduction)
  • k8s.io packages: 408 → 10 (97.5% reduction)
  • Cloud SDKs: 96 → 0 (100% removed)

See: https://www.datadoghq.com/blog/engineering/agent-go-binaries/

User-facing documentation

Testing and quality

  • the change is production ready: the change is GA, or otherwise the functionality is gated by a feature flag
  • CI results are inspected

Automated testing

  • added unit tests
  • added e2e tests
  • added regression tests
  • added compatibility tests
  • modified existing tests

How I validated my change

CI

@janisz janisz requested a review from a team as a code owner April 2, 2026 16:13
@janisz janisz marked this pull request as draft April 2, 2026 16:13
@janisz janisz force-pushed the optimize-roxagent-binary-size branch from b90bdcc to 7913594 Compare April 2, 2026 17:07
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

🚀 Build Images Ready

Images are ready for commit 805b97f. To use with deploy scripts:

export MAIN_IMAGE_TAG=4.11.x-552-g805b97f6ae

@janisz janisz force-pushed the optimize-roxagent-binary-size branch from 2057930 to 25cb6d8 Compare April 3, 2026 07:34
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 3, 2026

Codecov Report

❌ Patch coverage is 46.12903% with 167 lines in your changes missing coverage. Please review.
✅ Project coverage is 49.59%. Comparing base (7149d5a) to head (805b97f).
⚠️ Report is 10 commits behind head on master.

Files with missing lines Patch % Lines
compliance/node/vm/indexer.go 2.64% 147 Missing ⚠️
compliance/node/vm/convert.go 87.42% 13 Missing and 7 partials ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##           master   #19804    +/-   ##
========================================
  Coverage   49.59%   49.59%            
========================================
  Files        2763     2765     +2     
  Lines      208181   208581   +400     
========================================
+ Hits       103256   103455   +199     
- Misses      97261    97452   +191     
- Partials     7664     7674    +10     
Flag Coverage Δ
go-unit-tests 49.59% <46.12%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Consolidate 8 separate binaries into a single binary using BusyBox-style
dispatch pattern to reduce image size by ~54-64% (from ~1.1GB to ~400-500MB).

**Changes:**
- Refactor each component to use app package pattern:
  - migrator/app, compliance/cmd/compliance/app
  - sensor/admission-control/app, sensor/kubernetes/app
  - sensor/upgrader/app, config-controller/app
  - compliance/virtualmachines/roxagent/app
- Add build tags (//go:build !centralall) to component main.go files
- Update central/main.go with BusyBox dispatcher and app package imports
- Modify Makefile to build only central binary with centralall tag
- Update Dockerfile to create symlinks instead of copying separate binaries

**Implementation:**
Each component now has:
1. app/app.go - Contains Run() function with main logic
2. main.go - Thin wrapper that calls app.Run() (excluded with centralall tag)

central/main.go dispatcher checks os.Args[0] and routes to appropriate app.Run().

**Testing:**
All refactored components validated with gopls - no diagnostics.
Individual components still build independently without centralall tag.

**Benefits:**
- 54-64% image size reduction
- Better code organization (app logic separate from entry point)
- Improved testability (app.Run() can be tested directly)
- No code duplication
- Minimal changes to existing code

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>

Remove unnecessary build tags from BusyBox consolidation

The //go:build !centralall tags were not needed because Go's package system
naturally handles the separation:
- Building ./central only compiles central package + its dependencies (app packages)
- Component main.go files are in separate packages and won't be included
- Simpler implementation without conditional compilation

This makes the code cleaner and easier to understand.

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>

Update Konflux Dockerfile for BusyBox consolidation

Apply the same BusyBox-style consolidation to the Konflux build:
- Copy only the central binary instead of 8 separate binaries
- Create symlinks for all component entry points
- Matches changes made to image/rhel/Dockerfile

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • With the new BusyBox-style consolidation, multiple apps that previously lived in separate binaries now share the global flag.CommandLine; please audit and, where necessary, move to per-app flag.FlagSets or gate init-time flag registration so that the central CLI surface and flag behavior do not unintentionally change or conflict with flags from other apps (e.g., upgrader’s -workflow).
  • The dispatcher in central/main.go silently falls back to running Central on unknown argv[0] values; consider logging an error and exiting (or at least warning) when an unrecognized binary name is used to avoid misconfigurations where, for example, a typo in the symlink name results in Central starting instead of the intended component.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- With the new BusyBox-style consolidation, multiple apps that previously lived in separate binaries now share the global `flag.CommandLine`; please audit and, where necessary, move to per-app `flag.FlagSet`s or gate init-time flag registration so that the `central` CLI surface and flag behavior do not unintentionally change or conflict with flags from other apps (e.g., upgrader’s `-workflow`).
- The dispatcher in `central/main.go` silently falls back to running Central on unknown `argv[0]` values; consider logging an error and exiting (or at least warning) when an unrecognized binary name is used to avoid misconfigurations where, for example, a typo in the symlink name results in Central starting instead of the intended component.

## Individual Comments

### Comment 1
<location path="central/main.go" line_range="235" />
<code_context>
 	"github.com/stackrox/rox/pkg/utils"
 	pkgVersion "github.com/stackrox/rox/pkg/version"
+
+	// BusyBox-style consolidation - import app packages
+	complianceapp "github.com/stackrox/rox/compliance/cmd/compliance/app"
+	roxagentapp "github.com/stackrox/rox/compliance/virtualmachines/roxagent/app"
</code_context>
<issue_to_address>
**issue (bug_risk):** Importing app packages into central causes their init() side effects (like memlimit.SetMemoryLimit) to run in the central process as well.

Some of these `.../app` packages (e.g. compliance, admission-control, k8s sensor, roxagent) call `memlimit.SetMemoryLimit()` or other side-effectful logic in `init()`. With these imports now always pulled into the central binary, that logic also runs when central starts, not just for the symlinked binaries. To avoid unintended impact on central’s own resource configuration, consider moving such calls out of `init()` and into the respective `Run()` paths so they only execute when that app is actually invoked.
</issue_to_address>

### Comment 2
<location path="central/main.go" line_range="1107-1116" />
<code_context>
+
+func main() {
+	// BusyBox-style dispatcher: check how we were called
+	binaryName := filepath.Base(os.Args[0])
+
+	switch binaryName {
+	case "central":
+		Main()
+	case "migrator":
+		migratorMain()
+	case "compliance":
+		complianceMain()
+	case "kubernetes-sensor":
+		kubernetesSensorMain()
+	case "sensor-upgrader":
+		sensorUpgraderMain()
+	case "admission-control":
+		admissionControlMain()
+	case "config-controller":
+		configControllerMain()
+	case "roxagent":
+		roxagentMain()
+	default:
+		// Default to central if called with unknown name
+		Main()
+	}
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Defaulting to central on unknown binary names may hide misconfigurations.

Because any unexpected `binaryName` falls through to `Main()`, misnamed symlinks or typos will silently run central instead of failing fast. It would be safer to log a clear warning or exit non‑zero when the name is not recognized, so configuration issues are detected rather than masked.

Suggested implementation:

```golang
func main() {
	// BusyBox-style dispatcher: check how we were called
	binaryName := filepath.Base(os.Args[0])

	switch binaryName {
	case "central":
		Main()
	case "migrator":
		migratorMain()
	case "compliance":
		complianceMain()
	case "kubernetes-sensor":
		kubernetesSensorMain()
	case "sensor-upgrader":
		sensorUpgraderMain()
	case "admission-control":
		admissionControlMain()
	case "config-controller":
		configControllerMain()
	case "roxagent":
		roxagentMain()
	default:
		log.Errorf("unknown binary name %q; expected one of [central, migrator, compliance, kubernetes-sensor, sensor-upgrader, admission-control, config-controller, roxagent]", binaryName)
		os.Exit(1)
	}
}

```

You will also need to:
1. Ensure the import block at the top of `central/main.go` includes:
   - `"os"`
   - `"path/filepath"`
2. Confirm that `log` is a logger with `Errorf` available (e.g. from your existing logging package). If not, adjust the logging call to match your logging API.
3. Make sure functions like `Main`, `kubernetesSensorMain`, `sensorUpgraderMain`, and `admissionControlMain` are defined in this file or imported from the appropriate packages.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

"github.com/stackrox/rox/pkg/utils"
pkgVersion "github.com/stackrox/rox/pkg/version"

// BusyBox-style consolidation - import app packages
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (bug_risk): Importing app packages into central causes their init() side effects (like memlimit.SetMemoryLimit) to run in the central process as well.

Some of these .../app packages (e.g. compliance, admission-control, k8s sensor, roxagent) call memlimit.SetMemoryLimit() or other side-effectful logic in init(). With these imports now always pulled into the central binary, that logic also runs when central starts, not just for the symlinked binaries. To avoid unintended impact on central’s own resource configuration, consider moving such calls out of init() and into the respective Run() paths so they only execute when that app is actually invoked.

Comment on lines +1107 to +1116
binaryName := filepath.Base(os.Args[0])

switch binaryName {
case "central":
Main()
case "migrator":
migratorMain()
case "compliance":
complianceMain()
case "kubernetes-sensor":
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (bug_risk): Defaulting to central on unknown binary names may hide misconfigurations.

Because any unexpected binaryName falls through to Main(), misnamed symlinks or typos will silently run central instead of failing fast. It would be safer to log a clear warning or exit non‑zero when the name is not recognized, so configuration issues are detected rather than masked.

Suggested implementation:

func main() {
	// BusyBox-style dispatcher: check how we were called
	binaryName := filepath.Base(os.Args[0])

	switch binaryName {
	case "central":
		Main()
	case "migrator":
		migratorMain()
	case "compliance":
		complianceMain()
	case "kubernetes-sensor":
		kubernetesSensorMain()
	case "sensor-upgrader":
		sensorUpgraderMain()
	case "admission-control":
		admissionControlMain()
	case "config-controller":
		configControllerMain()
	case "roxagent":
		roxagentMain()
	default:
		log.Errorf("unknown binary name %q; expected one of [central, migrator, compliance, kubernetes-sensor, sensor-upgrader, admission-control, config-controller, roxagent]", binaryName)
		os.Exit(1)
	}
}

You will also need to:

  1. Ensure the import block at the top of central/main.go includes:
    • "os"
    • "path/filepath"
  2. Confirm that log is a logger with Errorf available (e.g. from your existing logging package). If not, adjust the logging call to match your logging API.
  3. Make sure functions like Main, kubernetesSensorMain, sensorUpgraderMain, and admissionControlMain are defined in this file or imported from the appropriate packages.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 3, 2026

📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • Refactor

    • Reorganized internal build and execution architecture to use a unified binary dispatcher pattern, consolidating multiple component binaries into a single entry point
    • Updated container image construction to use symlink-based binary routing instead of copying individual binaries
    • Extracted component initialization logic into modular entry points for improved code organization
  • Chores

    • Streamlined the build configuration to simplify binary compilation

Walkthrough

This refactoring restructures multiple microservices to use a BusyBox-style architecture where multiple service binaries are symlinks to a single central binary. Each service's initialization logic is extracted into a Run() function in a dedicated app package, and main() functions are simplified to dispatch based on the executable name or delegate to the extracted entrypoint.

Changes

Cohort / File(s) Summary
Build System & Main Dispatch
Makefile, central/main.go
Simplified build to compile only central and operator/cmd instead of individual binaries. Added dispatcher in central/main.go that routes execution based on os.Args[0] basename, renamed original main() to Main(), and created wrapper functions for each service (migratorMain, complianceMain, etc.).
Compliance Service
compliance/cmd/compliance/app/app.go, compliance/cmd/compliance/main.go, compliance/virtualmachines/roxagent/app/app.go, compliance/virtualmachines/roxagent/main.go
Extracted compliance and roxagent initialization into new app.Run() functions. Compliance app sets up continuous profiling, node indexers, and message handlers. Roxagent app manages signal handling for graceful shutdown. Both main.go files simplified to single app.Run() calls.
Config Controller Service
config-controller/app/app.go, config-controller/main.go
Moved all controller-runtime setup—including flag parsing, TLS configuration, metrics/webhook servers, manager construction, reconciler registration, and health probes—into config-controller/app/app.go. Main entrypoint reduced to single function call.
Migrator Service
migrator/app/app.go, migrator/app/upgrade.go, migrator/main.go
Created migrator/app/app.go with Run() entrypoint orchestrating profiling, configuration loading, database existence checks with retry logic, and upgrade execution. Updated upgrade.go package declaration from main to app. Simplified main.go to call app.Run().
Sensor Components
sensor/admission-control/app/app.go, sensor/admission-control/app/certs.go, sensor/admission-control/main.go, sensor/kubernetes/app/app.go, sensor/kubernetes/main.go, sensor/upgrader/app/app.go, sensor/upgrader/main.go
Extracted startup logic for three sensor services into dedicated app.Run() functions. Admission control handles signal-driven graceful shutdown and gRPC server wiring. Kubernetes sensor manages TLS, metrics, and Kubernetes client initialization. Upgrader handles workflow selection and context creation. Updated certs.go package declaration from main to app. All main.go files reduced to single delegation calls.
Container Images
image/rhel/Dockerfile, image/rhel/konflux.Dockerfile
Replaced individual binary COPY instructions with single central binary copy and created BusyBox-style symlinks under /stackrox/bin/ and /stackrox/ directories pointing all service names (migrator, compliance, kubernetes-sensor, etc.) to /stackrox/central.
Scripts
scripts/check-image-version.sh
Updated git grep pattern to match version strings with trailing characters (...-<version>.*) before extracting numeric X.Y.Z version segments.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title directly summarizes the main objective: optimizing roxagent binary size through package splitting, which is the core focus reflected throughout the changeset.
Description check ✅ Passed The description is mostly complete with problem statement, solution approach, and quantified results. However, the 'How I validated my change' section only states 'CI' without substantive detail, and testing checkboxes remain unchecked without explanation of why testing was skipped.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch optimize-roxagent-binary-size

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

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (1)
Makefile (1)

486-493: ⚠️ Potential issue | 🔴 Critical

Remove obsolete binary copies from copy-go-binaries-to-image-dir.

Lines 675-680 attempt to copy binaries (migrator, kubernetes, upgrader, admission-control, compliance, roxagent) that are no longer built by main-build-nodeps. This will fail the build when docker-build-main-image executes. Additionally, config-controller on line 660 is also not built in the normal flow.

The Dockerfile already handles this correctly by creating symlinks to the central binary (lines 86-94), so these copy commands are unnecessary and should be removed.

Lines to remove from copy-go-binaries-to-image-dir target
cp bin/linux_$(GOARCH)/config-controller image/rhel/bin/config-controller
...
cp bin/linux_$(GOARCH)/migrator image/rhel/bin/migrator
cp bin/linux_$(GOARCH)/kubernetes        image/rhel/bin/kubernetes-sensor
cp bin/linux_$(GOARCH)/upgrader          image/rhel/bin/sensor-upgrader
cp bin/linux_$(GOARCH)/admission-control image/rhel/bin/admission-control
cp bin/linux_$(GOARCH)/compliance        image/rhel/bin/compliance
cp bin/linux_$(GOARCH)/roxagent          image/rhel/bin/roxagent
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Makefile` around lines 486 - 493, In the Makefile target
copy-go-binaries-to-image-dir remove the obsolete cp commands that copy binaries
no longer produced by main-build-nodeps: delete the lines that copy
config-controller, migrator, kubernetes, upgrader, admission-control,
compliance, and roxagent into image/rhel/bin; leave the remaining copies in
place and rely on the Dockerfile’s symlink logic (the symlinks created for
central) instead of adding these copy steps. Ensure the target still succeeds
when docker-build-main-image runs by verifying no other targets reference those
removed files.
🧹 Nitpick comments (3)
image/rhel/Dockerfile (1)

84-96: Inconsistent symlink placement for config-controller.

All other component symlinks are created in /stackrox/bin/ (migrator, compliance, kubernetes-sensor, sensor-upgrader, admission-control, roxagent), but config-controller is only created in /stackrox/ (line 92). For consistency and to match the pattern of other components, consider adding it to /stackrox/bin/ as well:

Suggested fix
 RUN mkdir -p /stackrox/bin && \
     ln -s /stackrox/central /stackrox/bin/migrator && \
     ln -s /stackrox/central /stackrox/bin/compliance && \
     ln -s /stackrox/central /stackrox/bin/kubernetes-sensor && \
     ln -s /stackrox/central /stackrox/bin/sensor-upgrader && \
     ln -s /stackrox/central /stackrox/bin/admission-control && \
     ln -s /stackrox/central /stackrox/bin/roxagent && \
+    ln -s /stackrox/central /stackrox/bin/config-controller && \
     ln -s /stackrox/central /stackrox/config-controller && \
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@image/rhel/Dockerfile` around lines 84 - 96, The Dockerfile creates
BusyBox-style symlinks for many components under /stackrox/bin but
`config-controller` is only linked at /stackrox/config-controller; add a symlink
in /stackrox/bin to match the pattern (i.e. create a link pointing to
/stackrox/central named /stackrox/bin/config-controller) so it aligns with the
other entries like migrator, compliance, kubernetes-sensor, sensor-upgrader,
admission-control, and roxagent.
sensor/kubernetes/app/app.go (1)

36-38: Move memlimit setup out of package init().

This package is now imported by the shared dispatcher, so init() runs even for non-sensor entrypoints. Keeping the memory-limit change inside Run() avoids that process-wide import side effect.

Possible fix
-func init() {
-	memlimit.SetMemoryLimit()
-}
-
 // Run is the main entry point for the kubernetes-sensor application.
 func Run() {
+	memlimit.SetMemoryLimit()
 	premain.StartMain()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sensor/kubernetes/app/app.go` around lines 36 - 38, The
memlimit.SetMemoryLimit() call inside the package init() causes a process-wide
side effect when this package is imported; remove the init() function and
instead invoke memlimit.SetMemoryLimit() at the start of the sensor's Run()
function so the memory limit is applied only for sensor entrypoints—ensure
SetMemoryLimit() is called early in Run() before other initialization and that
any tests or callers expecting init-side effects are updated/verified.
compliance/cmd/compliance/app/app.go (1)

17-19: Move memlimit setup out of package init().

With the new shared dispatcher, importing this package is enough to execute init(), even when the compliance entrypoint is not selected. That turns memlimit.SetMemoryLimit() into a process-wide side effect instead of compliance-only startup. Call it from Run() instead.

Possible fix
-func init() {
-	memlimit.SetMemoryLimit()
-}
-
 var (
 	log = logging.LoggerForModule()
 )
@@
 func Run() {
+	memlimit.SetMemoryLimit()
 	if err := continuousprofiling.SetupClient(continuousprofiling.DefaultConfig()); err != nil {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@compliance/cmd/compliance/app/app.go` around lines 17 - 19, The package
init() currently calls memlimit.SetMemoryLimit(), causing a process-wide side
effect on import; remove that call from init() and instead invoke
memlimit.SetMemoryLimit() at the start of the compliance entrypoint Run()
function so the memory limit is applied only when the compliance command
actually runs (update the init() to be empty/removed and add the call to
memlimit.SetMemoryLimit() as the first statement in Run()).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@central/main.go`:
- Around line 234-243: Remove the extra blank line preceding the BusyBox-style
consolidation import block so the import comment and the grouped imports are
contiguous; locate the import block that lists complianceapp, roxagentapp,
configcontrollerapp, migratorapp, admissioncontrolapp, kubernetessensorapp, and
sensorupgraderapp and delete the stray empty line above the comment so the style
check passes.

In `@migrator/app/app.go`:
- Around line 31-41: The profiling server currently binds to all interfaces in
startProfilingServer (Addr: ":6060") which exposes routes.DebugRoutes publicly;
change the bind target to localhost (e.g., "127.0.0.1:6060" or "[::1]:6060") or
make the listen address configurable via an explicit option so remote exposure
is opt-in; update the srv creation to use that local or configurable address and
ensure the ListenAndServe goroutine remains unchanged.

In `@scripts/check-image-version.sh`:
- Line 6: Update the two regexes in the git grep/grep pipeline so the patch
component accepts multi-digit numbers: change the first pattern fragment
'(stackrox|scanner)-(build|test)-[0-9]+\.[0-9]+\.[0-9].*' to
'(stackrox|scanner)-(build|test)-[0-9]+\.[0-9]+\.[0-9]+.*' and change the second
extraction regex '[0-9]+\.[0-9]+\.[0-9]' to '[0-9]+\.[0-9]+\.[0-9]+' (the
pipeline that writes to "$tmpfile"); this ensures versions like 1.2.34 are
captured correctly.

In `@sensor/upgrader/app/app.go`:
- Around line 14-17: The upgrader package currently registers flags at package
init time on the global flag.CommandLine via the blank import of
"sensor/upgrader/flags"; change this so flags are registered onto a dedicated
local flag.FlagSet inside the upgrader Run() function: create a new
flag.NewFlagSet(...) in Run(), update the sensor/upgrader/flags package to
expose a registration function that takes a *flag.FlagSet (e.g. RegisterFlags(fs
*flag.FlagSet)) and registers the -workflow and other flags on that fs, remove
the blank import from app.go and call flags.RegisterFlags(localFS) then parse
localFS as needed before using the parsed values (propagate the FlagSet or
parsed values into metarunner/runner/upgradectx usage if required).

---

Outside diff comments:
In `@Makefile`:
- Around line 486-493: In the Makefile target copy-go-binaries-to-image-dir
remove the obsolete cp commands that copy binaries no longer produced by
main-build-nodeps: delete the lines that copy config-controller, migrator,
kubernetes, upgrader, admission-control, compliance, and roxagent into
image/rhel/bin; leave the remaining copies in place and rely on the Dockerfile’s
symlink logic (the symlinks created for central) instead of adding these copy
steps. Ensure the target still succeeds when docker-build-main-image runs by
verifying no other targets reference those removed files.

---

Nitpick comments:
In `@compliance/cmd/compliance/app/app.go`:
- Around line 17-19: The package init() currently calls
memlimit.SetMemoryLimit(), causing a process-wide side effect on import; remove
that call from init() and instead invoke memlimit.SetMemoryLimit() at the start
of the compliance entrypoint Run() function so the memory limit is applied only
when the compliance command actually runs (update the init() to be empty/removed
and add the call to memlimit.SetMemoryLimit() as the first statement in Run()).

In `@image/rhel/Dockerfile`:
- Around line 84-96: The Dockerfile creates BusyBox-style symlinks for many
components under /stackrox/bin but `config-controller` is only linked at
/stackrox/config-controller; add a symlink in /stackrox/bin to match the pattern
(i.e. create a link pointing to /stackrox/central named
/stackrox/bin/config-controller) so it aligns with the other entries like
migrator, compliance, kubernetes-sensor, sensor-upgrader, admission-control, and
roxagent.

In `@sensor/kubernetes/app/app.go`:
- Around line 36-38: The memlimit.SetMemoryLimit() call inside the package
init() causes a process-wide side effect when this package is imported; remove
the init() function and instead invoke memlimit.SetMemoryLimit() at the start of
the sensor's Run() function so the memory limit is applied only for sensor
entrypoints—ensure SetMemoryLimit() is called early in Run() before other
initialization and that any tests or callers expecting init-side effects are
updated/verified.
🪄 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: Central YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 7b782a11-87eb-41c9-9210-b1fda565c8c8

📥 Commits

Reviewing files that changed from the base of the PR and between b61d01e and dcf49d8.

📒 Files selected for processing (21)
  • Makefile
  • central/main.go
  • compliance/cmd/compliance/app/app.go
  • compliance/cmd/compliance/main.go
  • compliance/virtualmachines/roxagent/app/app.go
  • compliance/virtualmachines/roxagent/main.go
  • config-controller/app/app.go
  • config-controller/main.go
  • image/rhel/Dockerfile
  • image/rhel/konflux.Dockerfile
  • migrator/app/app.go
  • migrator/app/upgrade.go
  • migrator/main.go
  • scripts/check-image-version.sh
  • sensor/admission-control/app/app.go
  • sensor/admission-control/app/certs.go
  • sensor/admission-control/main.go
  • sensor/kubernetes/app/app.go
  • sensor/kubernetes/main.go
  • sensor/upgrader/app/app.go
  • sensor/upgrader/main.go

Comment on lines +234 to 243

// BusyBox-style consolidation - import app packages
complianceapp "github.com/stackrox/rox/compliance/cmd/compliance/app"
roxagentapp "github.com/stackrox/rox/compliance/virtualmachines/roxagent/app"
configcontrollerapp "github.com/stackrox/rox/config-controller/app"
migratorapp "github.com/stackrox/rox/migrator/app"
admissioncontrolapp "github.com/stackrox/rox/sensor/admission-control/app"
kubernetessensorapp "github.com/stackrox/rox/sensor/kubernetes/app"
sensorupgraderapp "github.com/stackrox/rox/sensor/upgrader/app"
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix import formatting: pipeline failure due to extra blank line.

The CI style check failed because of an extra blank line before the BusyBox imports block.

Suggested fix
 	pkgVersion "github.com/stackrox/rox/pkg/version"
-
 	// BusyBox-style consolidation - import app packages
 	complianceapp "github.com/stackrox/rox/compliance/cmd/compliance/app"
🧰 Tools
🪛 GitHub Actions: Style

[error] 234-234: style-slim failed: Too many blank lines in imports (imports formatting check)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@central/main.go` around lines 234 - 243, Remove the extra blank line
preceding the BusyBox-style consolidation import block so the import comment and
the grouped imports are contiguous; locate the import block that lists
complianceapp, roxagentapp, configcontrollerapp, migratorapp,
admissioncontrolapp, kubernetessensorapp, and sensorupgraderapp and delete the
stray empty line above the comment so the style check passes.

Comment on lines +31 to +41
func startProfilingServer() {
handler := http.NewServeMux()
for path, debugHandler := range routes.DebugRoutes {
handler.Handle(path, debugHandler)
}
srv := &http.Server{Addr: ":6060", Handler: handler}
go func() {
if err := srv.ListenAndServe(); err != nil {
log.WriteToStderrf("Closing profiling server: %v", err)
}
}()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't expose debug routes on all interfaces by default.

Addr: ":6060" binds every routes.DebugRoutes handler to 0.0.0.0 with no auth. That is risky in-cluster. Bind this to localhost by default, or make remote exposure an explicit config choice.

Possible fix
-	srv := &http.Server{Addr: ":6060", Handler: handler}
+	srv := &http.Server{
+		Addr:              "127.0.0.1:6060",
+		Handler:           handler,
+		ReadHeaderTimeout: 5 * time.Second,
+	}
📝 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 startProfilingServer() {
handler := http.NewServeMux()
for path, debugHandler := range routes.DebugRoutes {
handler.Handle(path, debugHandler)
}
srv := &http.Server{Addr: ":6060", Handler: handler}
go func() {
if err := srv.ListenAndServe(); err != nil {
log.WriteToStderrf("Closing profiling server: %v", err)
}
}()
func startProfilingServer() {
handler := http.NewServeMux()
for path, debugHandler := range routes.DebugRoutes {
handler.Handle(path, debugHandler)
}
srv := &http.Server{
Addr: "127.0.0.1:6060",
Handler: handler,
ReadHeaderTimeout: 5 * time.Second,
}
go func() {
if err := srv.ListenAndServe(); err != nil {
log.WriteToStderrf("Closing profiling server: %v", err)
}
}()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@migrator/app/app.go` around lines 31 - 41, The profiling server currently
binds to all interfaces in startProfilingServer (Addr: ":6060") which exposes
routes.DebugRoutes publicly; change the bind target to localhost (e.g.,
"127.0.0.1:6060" or "[::1]:6060") or make the listen address configurable via an
explicit option so remote exposure is opt-in; update the srv creation to use
that local or configurable address and ensure the ListenAndServe goroutine
remains unchanged.

trap 'rm -f "${tmpfile}"' EXIT

git grep -E -o -h '(stackrox|scanner)-(build|test)-[0-9]+\.[0-9]+\.[0-9]' | grep -E -o '[0-9]+\.[0-9]+\.[0-9]' | sort -u >"$tmpfile"
git grep -E -o -h '(stackrox|scanner)-(build|test)-[0-9]+\.[0-9]+\.[0-9].*' | grep -E -o '[0-9]+\.[0-9]+\.[0-9]' | sort -u >"$tmpfile"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if any image tags in the repo have multi-digit patch versions that would be truncated
git grep -E -o -h '(stackrox|scanner)-(build|test)-[0-9]+\.[0-9]+\.[0-9]+' | grep -E '[0-9]+\.[0-9]+\.[0-9]{2,}' | head -20

Repository: stackrox/stackrox

Length of output: 43


🏁 Script executed:

# First, let's examine the full script to understand context
head -20 scripts/check-image-version.sh

Repository: stackrox/stackrox

Length of output: 448


🏁 Script executed:

# Search for actual image tags/versions in the codebase to see what formats exist
git grep -E -o '(stackrox|scanner)-(build|test)-[0-9]+\.[0-9]+\.[0-9]' | head -30

Repository: stackrox/stackrox

Length of output: 1595


🏁 Script executed:

# Also search for version patterns more broadly
git grep -E '[0-9]+\.[0-9]+\.[0-9]' -- '*.sh' '*.md' '*.yaml' '*.yml' | grep -E '(stackrox|scanner)' | head -20

Repository: stackrox/stackrox

Length of output: 2251


Regex pattern should accept multi-digit patch versions.

The extraction pattern [0-9]+\.[0-9]+\.[0-9] uses a single digit for the patch component. While the current codebase only uses single-digit patch versions (e.g., 0.5.3), this pattern would fail with multi-digit patches like 1.2.34, extracting 1.2.3 instead and causing false positives.

For robustness, both patterns should use [0-9]+ consistently:

Proposed fix
-git grep -E -o -h '(stackrox|scanner)-(build|test)-[0-9]+\.[0-9]+\.[0-9].*' | grep -E -o '[0-9]+\.[0-9]+\.[0-9]' | sort -u >"$tmpfile"
+git grep -E -o -h '(stackrox|scanner)-(build|test)-[0-9]+\.[0-9]+\.[0-9]+.*' | grep -E -o '[0-9]+\.[0-9]+\.[0-9]+' | sort -u >"$tmpfile"
📝 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
git grep -E -o -h '(stackrox|scanner)-(build|test)-[0-9]+\.[0-9]+\.[0-9].*' | grep -E -o '[0-9]+\.[0-9]+\.[0-9]' | sort -u >"$tmpfile"
git grep -E -o -h '(stackrox|scanner)-(build|test)-[0-9]+\.[0-9]+\.[0-9]+.*' | grep -E -o '[0-9]+\.[0-9]+\.[0-9]+' | sort -u >"$tmpfile"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/check-image-version.sh` at line 6, Update the two regexes in the git
grep/grep pipeline so the patch component accepts multi-digit numbers: change
the first pattern fragment
'(stackrox|scanner)-(build|test)-[0-9]+\.[0-9]+\.[0-9].*' to
'(stackrox|scanner)-(build|test)-[0-9]+\.[0-9]+\.[0-9]+.*' and change the second
extraction regex '[0-9]+\.[0-9]+\.[0-9]' to '[0-9]+\.[0-9]+\.[0-9]+' (the
pipeline that writes to "$tmpfile"); this ensures versions like 1.2.34 are
captured correctly.

Comment on lines +14 to +17
_ "github.com/stackrox/rox/sensor/upgrader/flags"
"github.com/stackrox/rox/sensor/upgrader/metarunner"
"github.com/stackrox/rox/sensor/upgrader/runner"
"github.com/stackrox/rox/sensor/upgrader/upgradectx"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd /tmp && find . -name "app.go" | grep -E "sensor/upgrader" 2>/dev/null | head -20

Repository: stackrox/stackrox

Length of output: 43


🏁 Script executed:

git ls-files | grep -E "sensor/upgrader" | head -30

Repository: stackrox/stackrox

Length of output: 1247


🏁 Script executed:

cat -n sensor/upgrader/app/app.go | head -50

Repository: stackrox/stackrox

Length of output: 1682


🏁 Script executed:

cat -n sensor/upgrader/flags/flags.go 2>/dev/null || echo "File not found, searching for it..."

Repository: stackrox/stackrox

Length of output: 771


🏁 Script executed:

find . -path "*/sensor/upgrader/flags*" -type f

Repository: stackrox/stackrox

Length of output: 93


🏁 Script executed:

rg -l "sensor/upgrader/app" --type go | head -20

Repository: stackrox/stackrox

Length of output: 100


🏁 Script executed:

rg "from.*sensor.*upgrader" --type go | grep -i "dispatcher\|main\|shared" | head -20

Repository: stackrox/stackrox

Length of output: 43


🏁 Script executed:

git log --oneline --all -20 | grep -i "upgrade\|dispatch\|shared"

Repository: stackrox/stackrox

Length of output: 43


🏁 Script executed:

find . -name "main.go" | xargs rg -l "upgrader" 2>/dev/null | head -10

Repository: stackrox/stackrox

Length of output: 104


🏁 Script executed:

find . -path "*/sensor/upgrader*" -name "main.go" -o -path "*/sensor/upgrader*" -name "*dispatcher*" -o -path "*/sensor/upgrader*" -name "*shared*" | head -20

Repository: stackrox/stackrox

Length of output: 129


🏁 Script executed:

cat -n sensor/upgrader/main.go | head -50

Repository: stackrox/stackrox

Length of output: 204


🏁 Script executed:

cat -n central/main.go | grep -A 10 -B 10 "upgrader" | head -50

Repository: stackrox/stackrox

Length of output: 1796


🏁 Script executed:

rg "upgrader/app" --type go -B 2 -A 2

Repository: stackrox/stackrox

Length of output: 558


🏁 Script executed:

cat -n central/main.go | sed -n '1100,1130p'

Repository: stackrox/stackrox

Length of output: 869


🏁 Script executed:

find . -path "*/app/app.go" | xargs rg -l "flag.String\|flag.Duration\|flag.Bool" | head -10

Repository: stackrox/stackrox

Length of output: 43


🏁 Script executed:

# Check other app packages for flag definitions
for f in $(find . -path "*/app/app.go" | head -10); do echo "=== $f ==="; rg "flag\.(String|Duration|Bool|Int)" "$f" 2>/dev/null | head -5; done

Repository: stackrox/stackrox

Length of output: 735


🏁 Script executed:

# Check if other app packages might register conflicting flag names
for f in compliance/cmd/compliance/app/app.go migrator/app/app.go sensor/admission-control/app/app.go sensor/kubernetes/app/app.go; do
  echo "=== $f ===";
  rg "flag\.(String|Duration|Bool|Int)" "$f" 2>/dev/null | grep -oP '"\K[^"]+(?=")' | sort | uniq;
done

Repository: stackrox/stackrox

Length of output: 214


🏁 Script executed:

# Check if flags package in upgrader registers flags at init time
rg "^var|^const" sensor/upgrader/flags/flags.go -A 1

Repository: stackrox/stackrox

Length of output: 127


🏁 Script executed:

# Verify that imports happen at the top level of central/main.go before main() execution
head -300 central/main.go | tail -100

Repository: stackrox/stackrox

Length of output: 3845


🏁 Script executed:

# Check if central/main.go calls flag.Parse() at the top level
rg "flag.Parse\(\)" central/main.go

Repository: stackrox/stackrox

Length of output: 43


🏁 Script executed:

# Verify the dispatcher flow: does it parse flags before or after dispatching?
grep -n "flag.Parse\|switch.*binaryName" central/main.go

Repository: stackrox/stackrox

Length of output: 86


🏁 Script executed:

# Check if there are any other app packages that also register flags at package init
for pkg in compliance sensor/admission-control sensor/kubernetes config-controller migrator; do
  pkg_path=$(find . -path "*/$pkg/app" -type d 2>/dev/null | head -1)
  if [ -n "$pkg_path" ]; then
    echo "=== $pkg ===";
    rg "flag\." "$pkg_path" 2>/dev/null | head -10;
  fi
done

Repository: stackrox/stackrox

Length of output: 736


🏁 Script executed:

# Check if central/main.go has flag.Parse() anywhere
cat -n central/main.go | grep -B 5 -A 5 "Parse\|switch"

Repository: stackrox/stackrox

Length of output: 351


🏁 Script executed:

# More thorough check: look at the entire main function
sed -n '1105,1140p' central/main.go

Repository: stackrox/stackrox

Length of output: 607


🏁 Script executed:

# Check config-controller/app/app.go to see if it also has this pattern
cat -n config-controller/app/app.go | head -80

Repository: stackrox/stackrox

Length of output: 3509


🏁 Script executed:

# Check if sensor/kubernetes/app or sensor/admission-control/app register flags at package init
for path in sensor/kubernetes/app sensor/admission-control/app; do
  if [ -f "$path/app.go" ]; then
    echo "=== $path/app.go ===";
    cat -n "$path/app.go" | head -40;
  fi
done

Repository: stackrox/stackrox

Length of output: 3102


🏁 Script executed:

# Check central/main.go Main() function to see if it calls flag.Parse()
sed -n '1050,1105p' central/main.go

Repository: stackrox/stackrox

Length of output: 1026


🏁 Script executed:

# Final verification: trace the execution when central/main.go is invoked as "sensor-upgrader"
# Does it parse flags before or after dispatch?
grep -n "Parse\|premain\|startup" central/main.go | head -20

Repository: stackrox/stackrox

Length of output: 128


🏁 Script executed:

# Double-check: what happens when central/main.go's Main() is called - does it ever call flag.Parse()?
grep -n "flag.Parse" central/main.go

Repository: stackrox/stackrox

Length of output: 43


🏁 Script executed:

# Check if there are any flag name collisions between upgrader flags and other registered flags
echo "=== Upgrader flags ===" && \
rg "flag\.(String|Duration|Bool|Int)\(" sensor/upgrader/app sensor/upgrader/flags 2>/dev/null | \
grep -oP '\(\s*"[^"]+' | grep -oP '"[^"]+' | sort && \
echo -e "\n=== Other app flags ===" && \
rg "flag\.(String|Duration|Bool|Int)Var?" config-controller/app 2>/dev/null | \
grep -oP '"[^"]+' | sort

Repository: stackrox/stackrox

Length of output: 362


🏁 Script executed:

# Verify: When imported into central/main.go, are upgrader flags accessible via flag.CommandLine?
rg "flag.CommandLine\|flag.Lookup" central/main.go sensor/upgrader

Repository: stackrox/stackrox

Length of output: 43


🏁 Script executed:

# Check if flag registration at package level happens during import
# Simulate what happens when central/main.go is compiled
python3 << 'EOF'
# When Go imports a package, all package-level var declarations with
# initializers are evaluated. flag.String() calls register to flag.CommandLine
# Let's verify this is the actual pattern:
import subprocess
result = subprocess.run(['rg', 'var\\s*\\(', 'sensor/upgrader/app/app.go', '-A', '5'], 
                       capture_output=True, text=True)
print("=== Package-level var block in upgrader/app ===")
print(result.stdout)

result = subprocess.run(['rg', 'var\\s*\\(', 'sensor/upgrader/flags/flags.go', '-A', '5'],
                       capture_output=True, text=True)
print("\n=== Package-level var block in upgrader/flags ===")
print(result.stdout)
EOF

Repository: stackrox/stackrox

Length of output: 645


🏁 Script executed:

# Verify: does central's main dispatcher ever access os.Args before Run() is called?
grep -n "os.Args\|flag\|Parse" central/main.go | head -20

Repository: stackrox/stackrox

Length of output: 184


🏁 Script executed:

# Key question: when central is invoked as "sensor-upgrader", what is os.Args[1:]?
# If user passes flags like "central -workflow=foo", flag.Parse() in sensorupgraderapp.Run()
# will parse them, and all registered flags (including config-controller's) are available
echo "=== Checking if there's any flag preprocessing before dispatch ===" && \
sed -n '1105,1110p' central/main.go

Repository: stackrox/stackrox

Length of output: 276


🏁 Script executed:

# Final verification: Does central's Main() function ever access upgrader flags?
# Or any other part of central code reference these flags?
rg "workflow|KubeConfigSource|KubeTimeout" --type go central/ 2>/dev/null | grep -v "upgrader" | head -10

Repository: stackrox/stackrox

Length of output: 1338


🏁 Script executed:

# Check if there's any dynamic flag registration that could cause collisions
rg "flag\.(String|Duration|Bool|Int)" --type go | grep -v "test\|upgrader\|config-controller\|compliance" | head -20

Repository: stackrox/stackrox

Length of output: 3504


🏁 Script executed:

# Verify the exact Go semantics: does importing a package execute package-level var initializers?
python3 << 'EOF'
print("In Go:")
print("1. When a package is imported, package-level var declarations are initialized")
print("2. flag.String() calls flag.String() which is equivalent to flag.CommandLine.String()")
print("3. flag.CommandLine is the default FlagSet for the entire process")
print("")
print("Therefore:")
print("- When central/main.go imports sensor/upgrader/app")
print("- sensor/upgrader/app's init code runs (package-level var block)")
print("- flag.String('workflow', ...) registers on the global flag.CommandLine")
print("- sensor/upgrader/flags is imported, also registering flags globally")
print("- These flags are now accessible process-wide, even when running central mode")
print("")
print("Risk assessment:")
print("- Current code: No collision (other packages don't register flags at init)")
print("- Future risk: HIGH (if any other imported package registers flags at init with same name)")
print("- Design issue: YES (violates single-responsibility, coupling imported packages)")
EOF

Repository: stackrox/stackrox

Length of output: 909


Use a dedicated FlagSet for upgrader flags instead of registering on the global default.

The upgrader's flags package and the -workflow flag are registered at package initialization time on the global flag.CommandLine when sensor/upgrader/app is imported. In the shared-binary build (central/main.go), all app packages are imported regardless of which mode is invoked. While no flag name collisions currently exist with other packages, this pattern creates a maintainability risk: any future flag additions to upgrader could collide with flags from other integrated apps. Additionally, it violates separation of concerns—the upgrader's private flags should not be visible globally.

Move flag registration into the Run() function using a dedicated flag.FlagSet, and have sensor/upgrader/flags register flags onto that local set instead. This is consistent with Go best practices and matches the pattern used by config-controller/app.

Also applies to: 20-24, 31-33

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sensor/upgrader/app/app.go` around lines 14 - 17, The upgrader package
currently registers flags at package init time on the global flag.CommandLine
via the blank import of "sensor/upgrader/flags"; change this so flags are
registered onto a dedicated local flag.FlagSet inside the upgrader Run()
function: create a new flag.NewFlagSet(...) in Run(), update the
sensor/upgrader/flags package to expose a registration function that takes a
*flag.FlagSet (e.g. RegisterFlags(fs *flag.FlagSet)) and registers the -workflow
and other flags on that fs, remove the blank import from app.go and call
flags.RegisterFlags(localFS) then parse localFS as needed before using the
parsed values (propagate the FlagSet or parsed values into
metarunner/runner/upgradectx usage if required).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant