Skip to content

feat: Obfuscate collected must-gather output with must-gather-clean [RHIDP-16944 ] - #411

Open
Fortune-Ndlovu wants to merge 6 commits into
redhat-developer:mainfrom
Fortune-Ndlovu:RHIDP-16944-integrate-must-gather-clean-as-automatic-sanitization-for-must-gather-output
Open

Fortune-Ndlovu wants to merge 6 commits into
redhat-developer:mainfrom
Fortune-Ndlovu:RHIDP-16944-integrate-must-gather-clean-as-automatic-sanitization-for-must-gather-output

Conversation

@Fortune-Ndlovu

@Fortune-Ndlovu Fortune-Ndlovu commented Sep 29, 2026 •

Copy link
Copy Markdown
Member

Description

After the existing secret sanitizer, the gather now runs must-gather-clean so IP addresses, MAC addresses, and discovered OpenShift and API domains are rewritten before the output is shared. ConfigMaps and Secrets stay in the gather, secret values are still redacted by the current sanitizer, and the reversible report.yaml is left out of the published tree. If obfuscation fails, the command exits with an error and the collected output is left unchanged. --no-obfuscate skips this step, and RHDH_OBFUSCATE_DOMAINS adds domains that cluster discovery misses.

Which issue(s) does this PR fix or relate to

PR acceptance criteria

  • Tests
  • Documentation

How to test changes / Special notes to the reviewer

Prerequisites

  • This branch checked out, with Go 1.26 (go from go.mod).
  • oc logged in to an OpenShift cluster where an RHDH operator instance is running. oc whoami must succeed. The account needs to read that namespace, plus dnses/cluster and the default ingress controller. A cluster-admin login does.
  • The namespace needs a Route or Ingress on the cluster apps domain. Discovery can still log Obfuscating N domain name(s) when those objects are missing, but the output then has no domain string to rewrite. Confirm with oc get route,ingress -n <rhdh-namespace>.

Run

cd rhdh-must-gather

oc whoami
oc get backstage -A

BASE_COLLECTION_PATH=/tmp/rhdh-mg-test \
  make run-local OPTS="--namespaces <rhdh-namespace>"

To include Secrets as well:

BASE_COLLECTION_PATH=/tmp/rhdh-mg-test \
  make run-local OPTS="--namespaces <rhdh-namespace> --with-secrets"

The command should exit 0. The log should include Obfuscating N domain name(s) plus IP and MAC addresses with N greater than 0.

Check the output

# Reversible map is not published
find /tmp/rhdh-mg-test -name report.yaml -print
grep -R "replacedWith:" /tmp/rhdh-mg-test && echo "reversible map leaked" || echo "no reversible map"

# Obfuscation artifacts
test -f /tmp/rhdh-mg-test/watermark.txt && echo watermark=ok
test -f /tmp/rhdh-mg-test/sanitization-report.txt && echo sanitization-report=ok
test ! -d /tmp/rhdh-mg-test/.obfuscated-staging && echo staging-removed=ok

# ConfigMaps are still there
find /tmp/rhdh-mg-test -path '*_configmaps/*.yaml' | wc -l

# Addresses and domains
grep -R "x-ipv4-" /tmp/rhdh-mg-test | wc -l
grep -R "domain0000000001" /tmp/rhdh-mg-test | wc -l

Expect a watermark, a sanitization report, no .obfuscated-staging directory, ConfigMaps still present, x-ipv4- tokens, and domain0000000001 in all-routes.txt or all-ingresses.txt when that namespace has a Route or Ingress. 127.0.0.1 stays as-is.

Skip obfuscation with OPTS="--namespaces <rhdh-namespace> --no-obfuscate". Secret sanitization still runs, and the log says obfuscation is disabled.

Run must-gather-clean after the existing secret sanitizer so collected artifacts are obfuscated before they are shared. IP and MAC addresses are replaced consistently in file contents and paths, and discovered OpenShift and API domains are rewritten while ConfigMaps and Secrets stay in the gather. The reversible report is left out of the published tree, a failed obfuscation keeps the collected output and fails the command, and --no-obfuscate skips this step.

RHIDP-16944

Signed-off-by: Fortune-Ndlovu <fndlovu@redhat.com>
@rhdh-qodo-merge

Copy link
Copy Markdown

PR Summary by Qodo

Obfuscate must-gather output after secret sanitization

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Obfuscate collected IPs, MACs, and discovered or configured domains before sharing the gather.
• Keep secret sanitization and collected resources; exclude the reversible report from published
 output.
• Add an opt-out flag, documentation, and tests for discovery, obfuscation, and failure handling.
Diagram

graph TD
  CLI["Gather CLI"] --> Collected["Collected tree"] --> Sanitizer["Secret sanitizer"] --> Cleaner["Clean subprocess"] --> Staging["Staged tree"] --> Published["Published output"]
  Discovery["Cluster domains"] --> Cleaner
  Cleaner -.-> Report["Private report"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Atomic parent-directory swap
  • ➕ Could preserve the original tree if publication fails partway through.
  • ➖ Requires a replaceable parent directory and suitable filesystem semantics, which may not hold for the gather output path.
2. Run the cleaner in-process
  • ➕ Avoids re-executing the gather binary.
  • ➖ The library's process-exit behavior could terminate the collector instead of returning a recoverable error.

Recommendation: Keep the subprocess boundary: it isolates library exits and lets cleaner failures leave the collected tree intact. Consider an atomic publication strategy if preserving the original tree through publication failures is a strict requirement; the current delete-then-move phase does not provide that guarantee.

Files changed (10) +883 / -20

Enhancement (4) +515 / -3
gather.goRun obfuscation after secret sanitization +23/-3

Run obfuscation after secret sanitization

• Extends the deferred post-collection pass to run obfuscation unless disabled. Joins obfuscation or abort errors into the command result.

internal/cli/gather.go

obfuscate.goIsolate must-gather-clean in a hidden subprocess +73/-0

Isolate must-gather-clean in a hidden subprocess

• Adds a hidden command for running the cleaner and a wrapper that re-executes the gather binary. This isolates library exits from the collecting process.

internal/cli/obfuscate.go

root.goRegister obfuscation CLI controls +3/-0

Register obfuscation CLI controls

• Adds the --no-obfuscate flag and registers the hidden obfuscate subcommand.

internal/cli/root.go

obfuscate.goDiscover domains and stage obfuscated output +416/-0

Discover domains and stage obfuscated output

• Builds a cleaner configuration from OpenShift, API-server, and user-supplied domains, then runs IP, MAC, and domain obfuscation. Keeps the reversible report outside the gather, rejects reports in cleaned output, and stages files before publishing them.

internal/obfuscate/obfuscate.go

Tests (2) +339 / -0
root_test.goTest CLI flag and cleaner subcommand +62/-0

Test CLI flag and cleaner subcommand

• Checks --no-obfuscate parsing and verifies that the hidden command rewrites an IP while writing its report outside the cleaned output.

internal/cli/root_test.go

obfuscate_test.goCover discovery, cleaning, and cleaner failures +277/-0

Cover discovery, cleaning, and cleaner failures

• Tests domain filtering and discovery, generated cleaner configuration, consistent address replacement, retained resources, and report exclusion. Confirms that a cleaner failure leaves the original files unchanged.

internal/obfuscate/obfuscate_test.go

Documentation (2) +25 / -1
README.mdDocument obfuscation controls and output +8/-0

Document obfuscation controls and output

• Adds the extra-domains environment variable and --no-obfuscate option to the usage documentation. Shows the obfuscation watermark in the output tree.

README.md

secret-collection-and-sanitization.mdExplain automatic obfuscation and sharing precautions +17/-1

Explain automatic obfuscation and sharing precautions

• Describes domain discovery, address rewriting, preserved resources, report exclusion, and the opt-out behavior. Reminds users to review gathers for names discovery missed.

docs/secret-collection-and-sanitization.md

Other (2) +4 / -16
go.modAdd must-gather-clean dependency +2/-0

Add must-gather-clean dependency

• Adds openshift/must-gather-clean v0.0.5 and its indirect YAML dependency.

go.mod

go.sumRecord dependency checksums +2/-16

Record dependency checksums

• Adds must-gather-clean checksums and removes obsolete Kubernetes dependency checksums.

go.sum

InProcess was only reached from tests. Those tests now call Clean directly, which is the function the gather command already uses.

Signed-off-by: Fortune-Ndlovu <fndlovu@redhat.com>
@rhdh-qodo-merge

rhdh-qodo-merge Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. A resource named report.yaml blocks the whole gather ✓ Resolved
Description
rejectReport walks the cleaned tree and fails whenever it finds any file literally named
report.yaml, without checking whether it is the reversible map or ordinary collected data.
Collectors name files after Kubernetes resources (cm.Name+".yaml", sec.Name+".yaml"), so a
ConfigMap, Secret, or custom resource named report makes obfuscation fail every time, which in
turn fails the entire gather command unless --no-obfuscate is passed.
Code

internal/obfuscate/obfuscate.go[R302-311]

+func rejectReport(root string) error {
+	return filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
+		if err != nil {
+			return err
+		}
+		if !d.IsDir() && d.Name() == reportFileName {
+			return fmt.Errorf("refusing to publish %s because it maps obfuscated values back to the original data", path)
+		}
+		return nil
+	})
Relevance

●●● Strong

Resource filenames can collide with report.yaml, causing a deterministic obfuscation failure; recent
correctness fixes are accepted.

PR-#388

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
workload.go writes ConfigMaps/Secrets as cm.Name+".yaml" / sec.Name+".yaml" into the collected
tree; operator.go and orchestrator.go do the same for other resource kinds. Any resource literally
named 'report' therefore produces a file called report.yaml inside outputPath, which rejectReport
(called from Apply at line 89-91 and from publish at lines 325 and 357) treats identically to the
reversible must-gather-clean report, aborting obfuscation and failing runGather via the errors.Join
call in gather.go.

internal/collector/workload.go[307-320]
internal/obfuscate/obfuscate.go[302-312]
internal/obfuscate/obfuscate.go[86-93]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`rejectReport` in `internal/obfuscate/obfuscate.go` rejects any file named `report.yaml` anywhere in the cleaned output tree, including legitimate collected Kubernetes resources that happen to be named `report` (e.g. a ConfigMap or Secret called `report`, producing `report.yaml` in `_configmaps/` or `_secrets/`). This causes obfuscation, and therefore the whole must-gather command, to fail whenever such a resource exists in the cluster.

## Fix Focus Areas
- internal/obfuscate/obfuscate.go[302-312]
- internal/obfuscate/obfuscate.go[86-93]
- internal/obfuscate/obfuscate.go[325-328]
- internal/obfuscate/obfuscate.go[357-357]

## Recommended Fix
Stop scanning the entire cleaned tree for any file named `report.yaml`. Since the real reversible report is always written to `reportDir` (a directory outside `outputPath`/`basePath`), remove the recursive walk in `rejectReport` and instead only check that `reportDir`'s report file was never copied into `outputPath`/`basePath` (e.g. verify `reportDir` and `outputPath` never overlap, or check a single well-known top-level path such as `filepath.Join(outputPath, reportFileName)` only if must-gather-clean could plausibly write it there directly). Ensure collected resource files with the same name are never mistaken for the sanitization report.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Chart gathers expose cluster domains ✗ Dismissed
Description
Discover requests OpenShift DNS and ingress-controller resources that rhdh-chart’s gather
ServiceAccount cannot read, then continues with those domains omitted. On chart-installed gathers,
base or ingress domains can remain in the output even though obfuscation reports success.
Code

internal/obfuscate/obfuscate.go[R161-162]

+	gvr := schema.GroupVersionResource{Group: "config.openshift.io", Version: "v1", Resource: "dnses"}
+	obj, err := client.Dynamic.Resource(gvr).Get(ctx, "cluster", metav1.GetOptions{})
Relevance

●● Moderate

Permission mismatch could leave domains unobfuscated, but cross-repository RBAC behavior makes
acceptance uncertain.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PR makes both resource requests but logs discovery errors and proceeds. The chart binds its
gather ServiceAccount to roles that grant neither requested resource.

rhdh-must-gather -> rhdh-chart
internal/obfuscate/obfuscate.go[124-150]
internal/obfuscate/obfuscate.go[153-190]
External repo: redhat-developer/rhdh-chart, charts/must-gather/templates/clusterrbac.yaml [26-35]
External repo: redhat-developer/rhdh-chart, charts/must-gather/templates/rbac.yaml [1-35]
External repo: redhat-developer/rhdh-chart, charts/must-gather/templates/deployment.yaml [23-25]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The chart’s gather ServiceAccount lacks permission to read either resource used for domain discovery, so default obfuscation can leave cluster domains unchanged.

## Fix Focus Areas
- internal/obfuscate/obfuscate.go[124-150]
- internal/obfuscate/obfuscate.go[153-190]
- charts/must-gather/templates/clusterrbac.yaml[26-33]
- charts/must-gather/templates/rbac.yaml[1-35]

## Recommended Fix
Coordinate an rhdh-chart RBAC update granting the required reads where the chart supports them, including cluster-scoped DNS access and ingress-controller access. Account for namespace-scoped installations that cannot perform cluster discovery, and make incomplete discovery explicit rather than treating it as fully obfuscated output.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Domain discovery can outlast its timeout ✗ Dismissed
Description
openShiftBaseDomain and openShiftIngressDomain call apiGroupPresent before their context-aware
resource requests, but that helper calls HasAPIGroup without passing the discovery context. If
API-group listing stalls, the ten-second discovery deadline cannot stop that call, delaying
obfuscation and command completion.
Code

internal/obfuscate/obfuscate.go[157]

+	present, err := apiGroupPresent(client, "config.openshift.io")
Relevance

●●● Strong

The discovery deadline is bypassed by a non-context-aware API-group check, a clear timeout
correctness defect.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Run creates a ten-second context, but both domain lookups first use HasAPIGroup, whose ServerGroups
call accepts no context from that deadline.

internal/obfuscate/obfuscate.go[47-53]
internal/obfuscate/obfuscate.go[153-163]
internal/obfuscate/obfuscate.go[173-183]
internal/kube/client.go[43-57]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new domain-discovery timeout does not cover API-group listing, which runs before the context-aware resource requests.
## Fix Focus Areas
- internal/obfuscate/obfuscate.go[153-200]
- internal/kube/client.go[43-57]
## Recommended Fix
Use a context-aware, deadline-bound API-group lookup for both domain checks, or otherwise enforce the discovery deadline on those requests.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. A stuck cleaner can hold the command open ✗ Dismissed
Description
runGather starts obfuscation with context.Background(), while runCleanSubprocess waits for the
child with exec.Command(...).Run() rather than a cancellable command. Once collection ends, the
discovery timeout does not cover the cleaner, so a stuck child has no application-level cancellation
or deadline.
Code

internal/cli/gather.go[65]

+		if oerr := obfuscate.Run(context.Background(), kubeClient, basePath, runCleanSubprocess); oerr != nil {
Relevance

●●● Strong

Cancellable subprocess and timeout concerns are clear reliability defects; recent partial-output
reliability feedback was accepted.

PR-#393

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The timeout context is used for discovery, but Apply invokes a cleaner with no context and the child
process is started with exec.Command.

internal/cli/gather.go[64-65]
internal/obfuscate/obfuscate.go[47-53]
internal/obfuscate/obfuscate.go[86-87]
internal/cli/obfuscate.go[55-60]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new cleaner subprocess has no cancellation or deadline after domain discovery finishes.
## Fix Focus Areas
- internal/cli/gather.go[64-68]
- internal/cli/obfuscate.go[43-60]
- internal/obfuscate/obfuscate.go[40-54]
## Recommended Fix
Propagate a context through the cleaning call and run the subprocess with context-aware cancellation and an appropriate deadline.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. A failed publish step can leave a half-swapped, mixed gather ✗ Dismissed
Description
publish deletes the original entries under basePath before moving staged obfuscated entries into
place one at a time, with no rollback if either operation fails. A partial RemoveAll or Rename
failure can leave the gather empty or mixed, while signal.Stop(sigCh) runs before the obfuscation
defer, allowing SIGINT/SIGTERM to terminate the process during the same swap.
Code

internal/obfuscate/obfuscate.go[R334-353]

+	for _, e := range entries {
+		if e.Name() == stagingDirName {
+			continue
+		}
+		if err := os.RemoveAll(filepath.Join(basePath, e.Name())); err != nil {
+			return fmt.Errorf("replacing collected output: %w", err)
+		}
+	}
+
+	staged, err := os.ReadDir(staging)
+	if err != nil {
+		return fmt.Errorf("reading staged obfuscated output: %w", err)
+	}
+	for _, e := range staged {
+		from := filepath.Join(staging, e.Name())
+		to := filepath.Join(basePath, e.Name())
+		if err := os.Rename(from, to); err != nil {
+			return fmt.Errorf("publishing obfuscated file %s: %w", e.Name(), err)
+		}
+	}
Relevance

●●● Strong

Non-atomic publishing can leave partial output; recent feedback protecting against partial gather
artifacts was accepted.

PR-#393

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
In internal/obfuscate/obfuscate.go, the deletion loop at lines 334–341 precedes the per-entry
rename loop at lines 347–353, and errors return without restoring deleted entries; the gather caller
only reports the error. In internal/cli/gather.go, signal.Stop(sigCh) is deferred after the
obfuscation defer, so LIFO defer execution unregisters the signal handler before obfuscation calls
publish.

internal/obfuscate/obfuscate.go[330-357]
internal/cli/gather.go[47-83]
internal/obfuscate/obfuscate.go[330-351]
internal/cli/gather.go[64-69]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`publish` deletes collected output before the staged replacement is fully installed. A filesystem error can leave the gather empty or mixed rather than preserving the original tree, and SIGINT/SIGTERM can terminate publication after the signal handler has been stopped.

## Fix Focus Areas
- internal/obfuscate/obfuscate.go[314-357]
- internal/cli/gather.go[47-83]

## Recommended Fix
Move the original `basePath` contents to a backup location before installing the staged files. Keep that backup until publication fully succeeds, then delete it; on any failure during the swap, restore the originals. Keep the SIGINT/SIGTERM handler active, or explicitly ignore or queue those signals, until the obfuscation defer in `runGather` has finished.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View medium (1)
6. Large chart gathers fail obfuscation ✗ Dismissed
Description
Apply creates its cleaned output under the OS default temporary directory rather than on the
volume backing basePath, and publish falls back from a cross-filesystem rename to copying the
full tree. With the chart’s 128Mi ephemeral-storage limit, that copy can exhaust /tmp during
default obfuscation, especially when --with-heap-dumps adds large .heapsnapshot files, despite
the 1Gi output volume.
Code

internal/obfuscate/obfuscate.go[70]

+	work, err := os.MkdirTemp("", "rhdh-must-gather-obfuscate-")
Relevance

●● Moderate

Cross-filesystem copying and heap-dump size create plausible resource risk, but deployment-specific
impact is uncertain.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
os.MkdirTemp("", ...) places the work directory under the container’s default temporary directory,
typically /tmp, while the chart mounts the output volume at /must-gather and limits
gather-container ephemeral storage to 128Mi. When publish cannot rename across those filesystems,
it uses copyTree to copy the full cleaned tree; that tree can include the heap snapshots collected
with --with-heap-dumps.

rhdh-must-gather -> rhdh-chart
internal/obfuscate/obfuscate.go[70-86]
internal/cli/gather.go[50-68]
internal/obfuscate/obfuscate.go[70-93]
internal/obfuscate/obfuscate.go[314-324]
internal/collector/heapdump.go[176-176]
External repo: redhat-developer/rhdh-chart, charts/must-gather/templates/deployment.yaml [148-150]
External repo: redhat-developer/rhdh-chart, charts/must-gather/values.yaml [119-138]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`Apply` stages the full cleaned gather on container ephemeral storage rather than the volume backing `basePath`. Publication can then fall back from a cross-filesystem rename to copying the tree, exhausting the chart’s limited ephemeral storage, particularly for gathers with heap snapshots.

## Fix Focus Areas
- internal/obfuscate/obfuscate.go[70-86]
- internal/obfuscate/obfuscate.go[314-357]
- charts/must-gather/values.yaml[119-138]
- charts/must-gather/templates/deployment.yaml[148-150]

## Recommended Fix
Create the cleaning work directory on a suitably sized volume and on the same filesystem as the publication destination, so `publish` can rename it without copying the full tree. Use a sibling of `basePath` where that location is on the mounted volume; otherwise coordinate the necessary mount or volume-size changes in rhdh-chart. Keep the small `reportDir` separate if appropriate, and preserve the intended failure behavior during publication.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

7. Wrapper scripts passing extra arguments now fail ✓ Resolved
Description
Adding newObfuscateCmd() as a subcommand via cmd.AddCommand changes cobra's default Args
validation for the root command, causing it to reject unexpected positional arguments as an "unknown
command" error where previously they were silently accepted; FParseErrWhitelist.UnknownFlags only
whitelists unrecognized flags, not positional arguments. Callers such as `oc adm must-gather --
/usr/bin/gather [args]` that pass stray positional tokens, or a wrapper script that mistakenly
forwards an extra token, will now break, and cobra also silently gains built-in help/completion
subcommands that shadow any collector named the same.
Code

internal/cli/root.go[94]

+	cmd.AddCommand(newObfuscateCmd())
Relevance

●●● Strong

Adding a Cobra child command changes positional-argument behavior; this is a deterministic
compatibility regression.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
root.go line 76 sets cmd.FParseErrWhitelist.UnknownFlags = true which only affects flag parsing, not
positional argument validation; cobra's default Args validator on a command with children rejects
unrecognized positional arguments as unknown subcommands once AddCommand is called.
TestUnknownFlagsAllowed in root_test.go only exercises an unknown flag, not a positional argument,
so this behavior change is untested.

internal/cli/root.go[76-94]
internal/cli/root_test.go[185-192]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Registering `newObfuscateCmd()` on the root command via `cmd.AddCommand` changes cobra's default argument validation, causing the root command to now reject unexpected positional arguments as unknown subcommands, a behavior change from before this PR.

## Fix Focus Areas
- internal/cli/root.go[94-94]

## Recommended Fix
Set `Args: cobra.ArbitraryArgs` on the root command definition to restore tolerance of positional arguments, and consider setting `CompletionOptions.DisableDefaultCmd = true` to avoid cobra auto-registering a `completion` subcommand that could collide with collector names.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
⚠️ Tickets: not configured — ticket URL found in PR but could not be fetched — check ticket provider credentials
✅ Compliance rules (platform): 7 rules
✅ Cross-repo context — repo relationships
  Explored: repo: redhat-developer/rhdh-chart (sha: fb6c0e6c) — View relationship
Review mode: ⚖️ Balanced: This push changes privacy-sensitive obfuscation and report-publication behavior, creating genuine correctness and data-exposure risk despite being localized.

Grey Divider

Tip of the day
💡 Did you know, you can add REVIEW.md to your repo root and Qodo follows it on every PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Previous reviews

Review updated until commit cb15337

Results up to commit 148c50b 🧠 Deep


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Action required
1. A resource named report.yaml blocks the whole gather ✓ Resolved
Description
rejectReport walks the cleaned tree and fails whenever it finds any file literally named
report.yaml, without checking whether it is the reversible map or ordinary collected data.
Collectors name files after Kubernetes resources (cm.Name+".yaml", sec.Name+".yaml"), so a
ConfigMap, Secret, or custom resource named report makes obfuscation fail every time, which in
turn fails the entire gather command unless --no-obfuscate is passed.
Code

internal/obfuscate/obfuscate.go[R302-311]

+func rejectReport(root string) error {
+	return filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
+		if err != nil {
+			return err
+		}
+		if !d.IsDir() && d.Name() == reportFileName {
+			return fmt.Errorf("refusing to publish %s because it maps obfuscated values back to the original data", path)
+		}
+		return nil
+	})
Relevance

●●● Strong

Resource filenames can collide with report.yaml, causing a deterministic obfuscation failure; recent
correctness fixes are accepted.

PR-#388

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
workload.go writes ConfigMaps/Secrets as cm.Name+".yaml" / sec.Name+".yaml" into the collected
tree; operator.go and orchestrator.go do the same for other resource kinds. Any resource literally
named 'report' therefore produces a file called report.yaml inside outputPath, which rejectReport
(called from Apply at line 89-91 and from publish at lines 325 and 357) treats identically to the
reversible must-gather-clean report, aborting obfuscation and failing runGather via the errors.Join
call in gather.go.

internal/collector/workload.go[307-320]
internal/obfuscate/obfuscate.go[302-312]
internal/obfuscate/obfuscate.go[86-93]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`rejectReport` in `internal/obfuscate/obfuscate.go` rejects any file named `report.yaml` anywhere in the cleaned output tree, including legitimate collected Kubernetes resources that happen to be named `report` (e.g. a ConfigMap or Secret called `report`, producing `report.yaml` in `_configmaps/` or `_secrets/`). This causes obfuscation, and therefore the whole must-gather command, to fail whenever such a resource exists in the cluster.

## Fix Focus Areas
- internal/obfuscate/obfuscate.go[302-312]
- internal/obfuscate/obfuscate.go[86-93]
- internal/obfuscate/obfuscate.go[325-328]
- internal/obfuscate/obfuscate.go[357-357]

## Recommended Fix
Stop scanning the entire cleaned tree for any file named `report.yaml`. Since the real reversible report is always written to `reportDir` (a directory outside `outputPath`/`basePath`), remove the recursive walk in `rejectReport` and instead only check that `reportDir`'s report file was never copied into `outputPath`/`basePath` (e.g. verify `reportDir` and `outputPath` never overlap, or check a single well-known top-level path such as `filepath.Join(outputPath, reportFileName)` only if must-gather-clean could plausibly write it there directly). Ensure collected resource files with the same name are never mistaken for the sanitization report.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Chart gathers expose cluster domains ✗ Dismissed
Description
Discover requests OpenShift DNS and ingress-controller resources that rhdh-chart’s gather
ServiceAccount cannot read, then continues with those domains omitted. On chart-installed gathers,
base or ingress domains can remain in the output even though obfuscation reports success.
Code

internal/obfuscate/obfuscate.go[R161-162]

+	gvr := schema.GroupVersionResource{Group: "config.openshift.io", Version: "v1", Resource: "dnses"}
+	obj, err := client.Dynamic.Resource(gvr).Get(ctx, "cluster", metav1.GetOptions{})
Relevance

●● Moderate

Permission mismatch could leave domains unobfuscated, but cross-repository RBAC behavior makes
acceptance uncertain.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PR makes both resource requests but logs discovery errors and proceeds. The chart binds its
gather ServiceAccount to roles that grant neither requested resource.

rhdh-must-gather -> rhdh-chart
internal/obfuscate/obfuscate.go[124-150]
internal/obfuscate/obfuscate.go[153-190]
External repo: redhat-developer/rhdh-chart, charts/must-gather/templates/clusterrbac.yaml [26-35]
External repo: redhat-developer/rhdh-chart, charts/must-gather/templates/rbac.yaml [1-35]
External repo: redhat-developer/rhdh-chart, charts/must-gather/templates/deployment.yaml [23-25]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The chart’s gather ServiceAccount lacks permission to read either resource used for domain discovery, so default obfuscation can leave cluster domains unchanged.

## Fix Focus Areas
- internal/obfuscate/obfuscate.go[124-150]
- internal/obfuscate/obfuscate.go[153-190]
- charts/must-gather/templates/clusterrbac.yaml[26-33]
- charts/must-gather/templates/rbac.yaml[1-35]

## Recommended Fix
Coordinate an rhdh-chart RBAC update granting the required reads where the chart supports them, including cluster-scoped DNS access and ingress-controller access. Account for namespace-scoped installations that cannot perform cluster discovery, and make incomplete discovery explicit rather than treating it as fully obfuscated output.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended
3. A stuck cleaner can hold the command open ✗ Dismissed
Description
runGather starts obfuscation with context.Background(), while runCleanSubprocess waits for the
child with exec.Command(...).Run() rather than a cancellable command. Once collection ends, the
discovery timeout does not cover the cleaner, so a stuck child has no application-level cancellation
or deadline.
Code

internal/cli/gather.go[65]

+		if oerr := obfuscate.Run(context.Background(), kubeClient, basePath, runCleanSubprocess); oerr != nil {
Relevance

●●● Strong

Cancellable subprocess and timeout concerns are clear reliability defects; recent partial-output
reliability feedback was accepted.

PR-#393

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The timeout context is used for discovery, but Apply invokes a cleaner with no context and the child
process is started with exec.Command.

internal/cli/gather.go[64-65]
internal/obfuscate/obfuscate.go[47-53]
internal/obfuscate/obfuscate.go[86-87]
internal/cli/obfuscate.go[55-60]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new cleaner subprocess has no cancellation or deadline after domain discovery finishes.
## Fix Focus Areas
- internal/cli/gather.go[64-68]
- internal/cli/obfuscate.go[43-60]
- internal/obfuscate/obfuscate.go[40-54]
## Recommended Fix
Propagate a context through the cleaning call and run the subprocess with context-aware cancellation and an appropriate deadline.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. A failed publish step can leave a half-swapped, mixed gather ✗ Dismissed
Description
publish deletes the original entries under basePath before moving staged obfuscated entries into
place one at a time, with no rollback if either operation fails. A partial RemoveAll or Rename
failure can leave the gather empty or mixed, while signal.Stop(sigCh) runs before the obfuscation
defer, allowing SIGINT/SIGTERM to terminate the process during the same swap.
Code

internal/obfuscate/obfuscate.go[R334-353]

+	for _, e := range entries {
+		if e.Name() == stagingDirName {
+			continue
+		}
+		if err := os.RemoveAll(filepath.Join(basePath, e.Name())); err != nil {
+			return fmt.Errorf("replacing collected output: %w", err)
+		}
+	}
+
+	staged, err := os.ReadDir(staging)
+	if err != nil {
+		return fmt.Errorf("reading staged obfuscated output: %w", err)
+	}
+	for _, e := range staged {
+		from := filepath.Join(staging, e.Name())
+		to := filepath.Join(basePath, e.Name())
+		if err := os.Rename(from, to); err != nil {
+			return fmt.Errorf("publishing obfuscated file %s: %w", e.Name(), err)
+		}
+	}
Relevance

●●● Strong

Non-atomic publishing can leave partial output; recent feedback protecting against partial gather
artifacts was accepted.

PR-#393

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
In internal/obfuscate/obfuscate.go, the deletion loop at lines 334–341 precedes the per-entry
rename loop at lines 347–353, and errors return without restoring deleted entries; the gather caller
only reports the error. In internal/cli/gather.go, signal.Stop(sigCh) is deferred after the
obfuscation defer, so LIFO defer execution unregisters the signal handler before obfuscation calls
publish.

internal/obfuscate/obfuscate.go[330-357]
internal/cli/gather.go[47-83]
internal/obfuscate/obfuscate.go[330-351]
internal/cli/gather.go[64-69]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`publish` deletes collected output before the staged replacement is fully installed. A filesystem error can leave the gather empty or mixed rather than preserving the original tree, and SIGINT/SIGTERM can terminate publication after the signal handler has been stopped.

## Fix Focus Areas
- internal/obfuscate/obfuscate.go[314-357]
- internal/cli/gather.go[47-83]

## Recommended Fix
Move the original `basePath` contents to a backup location before installing the staged files. Keep that backup until publication fully succeeds, then delete it; on any failure during the swap, restore the originals. Keep the SIGINT/SIGTERM handler active, or explicitly ignore or queue those signals, until the obfuscation defer in `runGather` has finished.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Domain discovery can outlast its timeout ✗ Dismissed
Description
openShiftBaseDomain and openShiftIngressDomain call apiGroupPresent before their context-aware
resource requests, but that helper calls HasAPIGroup without passing the discovery context. If
API-group listing stalls, the ten-second discovery deadline cannot stop that call, delaying
obfuscation and command completion.
Code

internal/obfuscate/obfuscate.go[157]

+	present, err := apiGroupPresent(client, "config.openshift.io")
Relevance

●●● Strong

The discovery deadline is bypassed by a non-context-aware API-group check, a clear timeout
correctness defect.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Run creates a ten-second context, but both domain lookups first use HasAPIGroup, whose ServerGroups
call accepts no context from that deadline.

internal/obfuscate/obfuscate.go[47-53]
internal/obfuscate/obfuscate.go[153-163]
internal/obfuscate/obfuscate.go[173-183]
internal/kube/client.go[43-57]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new domain-discovery timeout does not cover API-group listing, which runs before the context-aware resource requests.
## Fix Focus Areas
- internal/obfuscate/obfuscate.go[153-200]
- internal/kube/client.go[43-57]
## Recommended Fix
Use a context-aware, deadline-bound API-group lookup for both domain checks, or otherwise enforce the discovery deadline on those requests.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View medium (1)
6. Large chart gathers fail obfuscation ✗ Dismissed
Description
Apply creates its cleaned output under the OS default temporary directory rather than on the
volume backing basePath, and publish falls back from a cross-filesystem rename to copying the
full tree. With the chart’s 128Mi ephemeral-storage limit, that copy can exhaust /tmp during
default obfuscation, especially when --with-heap-dumps adds large .heapsnapshot files, despite
the 1Gi output volume.
Code

internal/obfuscate/obfuscate.go[70]

+	work, err := os.MkdirTemp("", "rhdh-must-gather-obfuscate-")
Relevance

●● Moderate

Cross-filesystem copying and heap-dump size create plausible resource risk, but deployment-specific
impact is uncertain.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
os.MkdirTemp("", ...) places the work directory under the container’s default temporary directory,
typically /tmp, while the chart mounts the output volume at /must-gather and limits
gather-container ephemeral storage to 128Mi. When publish cannot rename across those filesystems,
it uses copyTree to copy the full cleaned tree; that tree can include the heap snapshots collected
with --with-heap-dumps.

rhdh-must-gather -> rhdh-chart
internal/obfuscate/obfuscate.go[70-86]
internal/cli/gather.go[50-68]
internal/obfuscate/obfuscate.go[70-93]
internal/obfuscate/obfuscate.go[314-324]
internal/collector/heapdump.go[176-176]
External repo: redhat-developer/rhdh-chart, charts/must-gather/templates/deployment.yaml [148-150]
External repo: redhat-developer/rhdh-chart, charts/must-gather/values.yaml [119-138]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`Apply` stages the full cleaned gather on container ephemeral storage rather than the volume backing `basePath`. Publication can then fall back from a cross-filesystem rename to copying the tree, exhausting the chart’s limited ephemeral storage, particularly for gathers with heap snapshots.

## Fix Focus Areas
- internal/obfuscate/obfuscate.go[70-86]
- internal/obfuscate/obfuscate.go[314-357]
- charts/must-gather/values.yaml[119-138]
- charts/must-gather/templates/deployment.yaml[148-150]

## Recommended Fix
Create the cleaning work directory on a suitably sized volume and on the same filesystem as the publication destination, so `publish` can rename it without copying the full tree. Use a sibling of `basePath` where that location is on the mounted volume; otherwise coordinate the necessary mount or volume-size changes in rhdh-chart. Keep the small `reportDir` separate if appropriate, and preserve the intended failure behavior during publication.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational
7. Wrapper scripts passing extra arguments now fail ✓ Resolved
Description
Adding newObfuscateCmd() as a subcommand via cmd.AddCommand changes cobra's default Args
validation for the root command, causing it to reject unexpected positional arguments as an "unknown
command" error where previously they were silently accepted; FParseErrWhitelist.UnknownFlags only
whitelists unrecognized flags, not positional arguments. Callers such as `oc adm must-gather --
/usr/bin/gather [args]` that pass stray positional tokens, or a wrapper script that mistakenly
forwards an extra token, will now break, and cobra also silently gains built-in help/completion
subcommands that shadow any collector named the same.
Code

internal/cli/root.go[94]

+	cmd.AddCommand(newObfuscateCmd())
Relevance

●●● Strong

Adding a Cobra child command changes positional-argument behavior; this is a deterministic
compatibility regression.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
root.go line 76 sets cmd.FParseErrWhitelist.UnknownFlags = true which only affects flag parsing, not
positional argument validation; cobra's default Args validator on a command with children rejects
unrecognized positional arguments as unknown subcommands once AddCommand is called.
TestUnknownFlagsAllowed in root_test.go only exercises an unknown flag, not a positional argument,
so this behavior change is untested.

internal/cli/root.go[76-94]
internal/cli/root_test.go[185-192]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Registering `newObfuscateCmd()` on the root command via `cmd.AddCommand` changes cobra's default argument validation, causing the root command to now reject unexpected positional arguments as unknown subcommands, a behavior change from before this PR.

## Fix Focus Areas
- internal/cli/root.go[94-94]

## Recommended Fix
Set `Args: cobra.ArbitraryArgs` on the root command definition to restore tolerance of positional arguments, and consider setting `CompletionOptions.DisableDefaultCmd = true` to avoid cobra auto-registering a `completion` subcommand that could collide with collector names.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread internal/cli/gather.go Outdated
Comment thread internal/obfuscate/obfuscate.go Outdated
Comment thread internal/obfuscate/obfuscate.go Outdated
Comment thread internal/obfuscate/obfuscate.go
Comment thread internal/cli/root.go
Comment thread internal/obfuscate/obfuscate.go Outdated
Comment thread internal/obfuscate/obfuscate.go
A ConfigMap or Secret named report is written as report.yaml. Obfuscation was treating every file with that name as the reversible must-gather-clean report and failing the gather.

RHIDP-16944

Signed-off-by: Fortune-Ndlovu <fndlovu@redhat.com>
Registering a subcommand made Cobra reject stray positional tokens. Flags are unchanged, and the hidden obfuscate command still runs when it is named.

RHIDP-16944

Signed-off-by: Fortune-Ndlovu <fndlovu@redhat.com>
@Fortune-Ndlovu

Copy link
Copy Markdown
Member Author

/agentic_review

@rhdh-qodo-merge

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 4d762a6

@github-actions

Copy link
Copy Markdown
Contributor

PR images are available (for 1 week):

  1. quay.io/rhdh-community/rhdh-must-gather:pr-411
  2. quay.io/rhdh-community/rhdh-must-gather:pr-411-4d762a65a

errcheck fails the unit-test job when defer os.RemoveAll drops its error. Cleanup of the temporary directory cannot change the gather result.

RHIDP-16944

Signed-off-by: Fortune-Ndlovu <fndlovu@redhat.com>
@github-actions

Copy link
Copy Markdown
Contributor

PR images are available (for 1 week):

  1. quay.io/rhdh-community/rhdh-must-gather:pr-411
  2. quay.io/rhdh-community/rhdh-must-gather:pr-411-becf057e6

@rm3l

rm3l commented Oct 1, 2026

Copy link
Copy Markdown
Member

/cc

@openshift-ci
openshift-ci Bot requested a review from rm3l October 1, 2026 22:45
Comment thread docs/secret-collection-and-sanitization.md Outdated
Comment thread internal/cli/root.go
// The hidden obfuscate command makes this a parent command. Without an
// explicit Args func, Cobra then rejects positional tokens as unknown commands.
cmd.Args = cobra.ArbitraryArgs
cmd.AddCommand(newObfuscateCmd())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why do we need a separate cli command and run it in a subprocess? If the risk is that must-gather-clean might exit abruptly, I think it should be possible to take a similar approach as with the namespace-inspect (same process but hook using kcmdutil.BehaviorOnFatal for example).
Going in-process would eliminate those hundred of lines of collateral complexity the subprocess creates

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I looked at doing this in the same process, the way namespace inspect hooks BehaviorOnFatal. That hook only catches kubectl CheckErr. must-gather-clean calls klog.Exitf from its own error goroutine, and that calls os.Exit. A hook or a recover in this process does not catch that, so the whole gather would stop and the caller would not get a clear failure. The hidden obfuscate command is the same binary run again. If the child fails, the original files stay and the command says the output was not obfuscated.

Comment thread internal/obfuscate/obfuscate.go Outdated
//
// When discovery finds nothing, IP and MAC obfuscation still run. Set
// EnvDomains to supply names that discovery cannot see.
func Discover(ctx context.Context, client *kube.Client) []string {

@rm3l rm3l Oct 4, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

IIUC, this will only be useful on OCP clusters, right? What would be the behavior on non-OCP clusters? Maybe the discovery logic in this Discover() function could detect the platform and branch accordingly, like so:

  • OCP: current path
  • Non-OCP: read hosts from Ingress resources, plus the API server hostname.

Thoughts?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good point. On OpenShift we still read the DNS cluster base domain, the default ingress controller domain, and the API server hostname. On Kubernetes, and whenever those OpenShift domains cannot be read, we now read hosts from Ingress resources and from Routes in the namespaces being collected, plus the API hostname. When the OpenShift suffixes are found, we do not also add every individual Ingress host, because those names already sit under the cluster domains. This is in cb15337.

Comment thread internal/cli/root.go
flags.StringVar(&opts.heapDumpMethod, "heap-dump-method", "inspector", "Heap dump collection method: inspector or sigusr2")
flags.StringVar(&opts.heapDumpInstances, "heap-dump-instances", "", "Comma-separated list of instance names to collect heap dumps from")
flags.BoolVar(&opts.clusterInfo, "cluster-info", false, "Collect cluster-wide diagnostic information")
flags.BoolVar(&opts.noObfuscate, "no-obfuscate", false, "Skip IP, MAC, and domain obfuscation (secret sanitization still runs)")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would suggest not exposing this flag for now. In my understanding, the consistent replacements (x-ipv4-..., domain0000000001) should preserve the structure we need for debugging, so I guess most analysis should still work on obfuscated output.
If the need or complaints come later, we could consider adding it, but for now, I think it should just be the opinionated behavior to obfuscate (similar to the automatic sanitization which is done with no option to skip). WDYT?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I would like to keep the flag. Obfuscation is already the default, the same way sanitization always runs. The consistent tokens are enough for most debugging. Heap dumps collected with --with-heap-dumps go through this pass, and looking at a memory snapshot needs the real addresses. The flag is also the way out when the clean step fails and the command refuses to publish the result. The docs say to pass --no-obfuscate for heap snapshots and for local debugging.

OpenShift DNS and ingress-controller reads stay the source of cluster suffixes. When those objects cannot be read, discovery uses Ingress and Route hosts in the namespaces being collected, plus the API hostname. The two OpenShift field reads share one lookup.

RHIDP-16944

Signed-off-by: Fortune-Ndlovu <fndlovu@redhat.com>
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

PR images are available (for 1 week):

  1. quay.io/rhdh-community/rhdh-must-gather:pr-411
  2. quay.io/rhdh-community/rhdh-must-gather:pr-411-cb15337d0

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants