Skip to content

feat: support external diff tool in dry-run mode#2605

Open
p2pdkivenko wants to merge 3 commits intosiderolabs:mainfrom
p2pdkivenko:feat/external-diff-dry-run
Open

feat: support external diff tool in dry-run mode#2605
p2pdkivenko wants to merge 3 commits intosiderolabs:mainfrom
p2pdkivenko:feat/external-diff-dry-run

Conversation

@p2pdkivenko
Copy link
Copy Markdown
Contributor

Summary

  • Add OMNI_EXTERNAL_DIFF environment variable support for omnictl apply --dry-run and omnictl cluster template sync --dry-run, mirroring kubectl's KUBECTL_EXTERNAL_DIFF behavior
  • New client/pkg/execdiff package that abstracts diff rendering — built-in colorized unified diff or external tool invocation via temp directories (LIVE/MERGED)
  • Replace outdated diffmatchpatch library in apply.go with the better diff.Compute algorithm already used by template sync

Closes #2329

Test plan

  • omnictl apply -f test.yaml --dry-run shows colorized unified diff (built-in mode)
  • OMNI_EXTERNAL_DIFF="diff -u" omnictl apply -f test.yaml --dry-run invokes external diff with temp dirs
  • OMNI_EXTERNAL_DIFF="colordiff -N -u" omnictl apply -f test.yaml --dry-run works with args
  • omnictl cluster template sync -f template.yaml --dry-run works with both modes
  • Exit codes: 0 when no diff, 1 when diff found, >1 on error
  • go build ./client/... compiles
  • go test ./client/... passes

Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@talos-bot talos-bot moved this from To Do to In Review in Planning Mar 28, 2026
@Unix4ever Unix4ever requested a review from Copilot April 14, 2026 15:57
@Unix4ever
Copy link
Copy Markdown
Member

@claude review

Comment thread client/pkg/execdiff/execdiff.go
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds support for rendering diffs during dry-run operations using either a built-in colorized unified diff or an external diff tool configured via OMNI_EXTERNAL_DIFF, and aligns omnictl apply diff computation with the existing diff.Compute algorithm.

Changes:

  • Introduce client/pkg/execdiff to render diffs (built-in) or invoke an external diff tool via temp LIVE/MERGED dirs.
  • Wire Differ into omnictl apply --dry-run and omnictl cluster template sync --dry-run.
  • Remove diffmatchpatch dependency from apply and adjust YAML marshaling to return []byte.

Reviewed changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
client/pkg/template/operations/sync.go Adds optional Differ to SyncOptions and routes verbose diff rendering through it.
client/pkg/omnictl/cluster/template/sync.go Instantiates/flushed Differ for dry-run template sync.
client/pkg/omnictl/apply.go Instantiates/flushed Differ for dry-run apply; removes diffmatchpatch; updates verbose output paths.
client/pkg/execdiff/execdiff.go New diff-rendering abstraction (built-in unified diff or external command over temp dirs).
client/go.mod Drops github.com/sergi/go-diff dependency.
client/go.sum Cleans up sums after dependency removal.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread client/pkg/template/operations/sync.go
Comment thread client/pkg/execdiff/execdiff.go
Comment thread client/pkg/execdiff/execdiff.go
Comment thread client/pkg/omnictl/apply.go Outdated
Comment thread client/pkg/omnictl/apply.go Outdated
Comment thread client/pkg/omnictl/apply.go
Comment thread client/pkg/omnictl/cluster/template/sync.go
@Unix4ever
Copy link
Copy Markdown
Member

Unix4ever commented Apr 23, 2026

@p2pdkivenko please address unresolved feedback. If resolved we will review again and if it looks good after the fixes will merge it. Thank you

@Unix4ever Unix4ever moved this from In Review to On Hold in Planning Apr 23, 2026
@p2pdkivenko
Copy link
Copy Markdown
Contributor Author

@Unix4ever got it !

@p2pdkivenko
Copy link
Copy Markdown
Contributor Author

@Unix4ever Addressed all the confirmed feedback in 7097292, then merged latest main:

Exit-code contract (omnictl apply --dry-run + omnictl cluster template sync --dry-run)

  • Differ.Flush() hasDiff is now propagated to the command's exit status via a new execdiff.ErrDifferencesFound sentinel
  • cmd/omnictl/main.go maps the sentinel to exit 1 silently; real errors print and exit 2, matching the documented 0 = no diff / 1 = diff / >1 = error contract the help text advertises

Filename sanitization (path-escape concern Copilot raised on sync.go:241 and execdiff.go:123)

  • New execdiff.SanitizeFilename replaces /, \, .., :, and \0 with _ and rejects empty/dot parts
  • Applied in both resourceFilename (apply.go) and renderDiff (operations/sync.go)
  • Differ.AddDiff now validates filenames in external mode (rejects absolute paths, separators, traversal); Flush re-validates as defense in depth

Verbose-mode diff regression (apply.go:245)

  • Non-dry-run --verbose was printing raw old/new YAML blobs instead of a diff (since we removed diffmatchpatch). It now uses the same diff.Compute + colorized renderer as the dry-run path, so verbose output is identical with or without --dry-run

Tests (the missing-tests comment)

  • Added execdiff/execdiff_test.go covering: sanitize rules, built-in no-diff / diff / create / delete rendering, external-mode filename validation (relative/abs/traversal), external-diff exit-code mapping (0/1/>1) via shell stubs, and empty-OMNI_EXTERNAL_DIFF handling

On the "no temp files" wish — unfortunately every external diff I surveyed (diff, colordiff, diff-so-fancy, delta) is file-oriented by design, so I left the temp-dir approach but made it safe.

Tests + go build ./... green on both client and root modules.

@Unix4ever
Copy link
Copy Markdown
Member

I tested omnictl and now there are more bugs:

  • the error messages are duplicated when you run the command
 → omnictl cluster template diff -f clusters/cluster.yaml -v
Error: unknown shorthand flag: 'v' in -v
Error: unknown shorthand flag: 'v' in -v
  • It doesn't look like the diff tool env var is used:
 → _out/omnictl-linux-amd64 cluster template diff -f clusters/cluster.yaml
>>> Test, env variable is set: asdfs # checked that it's set.
--- /dev/null
+++ Clusters.omni.sidero.dev(default/example)
@@ -0,0 +1,21 @@
+metadata:
+  namespace: default
+  type: Clusters.omni.sidero.dev
+  id: example
+  version: undefined
+  owner:
+  phase: running
+  created: 2026-04-30T16:17:14+03:00
...

Add OMNI_EXTERNAL_DIFF environment variable support for `omnictl apply
--dry-run` and `omnictl cluster template sync --dry-run`, mirroring
kubectl's KUBECTL_EXTERNAL_DIFF behavior.

When set, the external diff program is invoked with two temp directory
paths containing old (LIVE) and new (MERGED) resource YAML files.
Exit codes follow the convention: 0=no diff, 1=has diff, >1=error.

When unset, a built-in colorized unified diff is used (replacing the
previous diffmatchpatch-based output in apply.go with the better
diff.Compute algorithm already used by template sync).

Closes siderolabs#2329
- Sanitize resource filenames used in external-diff temp directories to
  prevent path escape via IDs containing '/', '\\', ':', or '..'.
  Introduces execdiff.SanitizeFilename and validateFilename enforced on
  entry; defense-in-depth re-validation at flush time.
- Propagate Differ.Flush() hasDiff to command exit status: return the
  new execdiff.ErrDifferencesFound sentinel from 'omnictl apply' and
  'omnictl cluster template sync', map it to exit 1 silently in main;
  real errors exit 2. Matches documented kubectl-style contract
  (0 = no diff, 1 = diff, >1 = error).
- Restore unified diff output in non-dry-run verbose mode (regression
  from diffmatchpatch removal): reuse the same diff.Compute-based
  renderer as the dry-run path so verbose apply is consistent.
- Extend template-sync help text with the OMNI_EXTERNAL_DIFF usage and
  exit-status contract.
- Add execdiff unit tests covering sanitize rules, built-in diff
  rendering, filename validation in external mode, and external-diff
  exit-code mapping (0 / 1 / >1) via shell stubs.

Signed-off-by: Daniil Kivenko <daniil.kivenko@p2p.org>
Two bugs reported on the PR after manual omnictl testing:

1. Error messages were printed twice (once by cobra, once by main):
     omnictl cluster template diff -f f.yaml -v
     Error: unknown shorthand flag: 'v' in -v
     Error: unknown shorthand flag: 'v' in -v
   Cobra already prints flag-parse and RunE errors, so main now only
   maps ErrDifferencesFound -> exit 1 silently and exits 2 for any
   other error. The dry-run RunE handlers set SilenceErrors only when
   returning the sentinel, leaving cobra's normal error path intact
   for real failures.

2. OMNI_EXTERNAL_DIFF was not honored by 'cluster template diff'
   (only by '... sync --dry-run' and 'apply --dry-run'). DiffTemplate
   now takes a *execdiff.Differ and routes its output through the
   shared renderDiff helper, so the env var works for the diff
   subcommand too. Help text updated to document this.

Also restore the missing 'os' import in operations/sync.go that was
lost during the rebase merge with the latest os.Root signature.
@p2pdkivenko p2pdkivenko force-pushed the feat/external-diff-dry-run branch from a99df4d to b7b9ed5 Compare May 6, 2026 19:03
@p2pdkivenko
Copy link
Copy Markdown
Contributor Author

@Unix4ever fixed 👾

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

Labels

None yet

Projects

Status: On Hold

Development

Successfully merging this pull request may close these issues.

[feature] omnictl diff support

4 participants