Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ Flags:
-C, --context int output NUM lines of context around changes (default -1)
--detailed-exitcode return a non-zero exit code when there are changes
--devel use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored.
--diff-tool string command used to compare the manifests instead of the built-in --output renderers (can also be set via the env var HELM_DIFF_TOOL). The old and the new manifest file paths are appended as the last two arguments
--disable-openapi-validation disables rendered templates validation against the Kubernetes OpenAPI Schema
--disable-validation disables rendered templates validation against the Kubernetes cluster you are currently pointing to. This is the same validation performed on an install
--dry-run string[="client"] --dry-run, --dry-run=client, or --dry-run=true disables cluster access and show diff as if it was install. Implies --install, --reset-values, and --disable-validation. --dry-run=server enables the cluster access with helm-get and the lookup template function.
Expand Down Expand Up @@ -219,6 +220,35 @@ helm diff upgrade prod api ./charts/api --output structured

When a kind is suppressed via `--suppress`, `changesSuppressed` is set to `true` and field details are omitted. Nested metadata such as labels show the container path (`metadata.labels`) and expose the label key through the `field` property (for example `app.kubernetes.io/version`).

### External diff tool

Set `--diff-tool` to a command and helm-diff renders the diff with that command instead of its built-in renderers. It writes the old and the new manifests into two temporary files and appends their paths as the last two arguments:

```shell
# any tool that accepts two file paths works
helm diff upgrade api ./charts/api --diff-tool "diff -u -N"
helm diff upgrade api ./charts/api --diff-tool "difft --language yaml"
helm diff upgrade api ./charts/api --diff-tool "git --no-pager diff --no-index --color"
helm diff upgrade api ./charts/api --diff-tool "delta --side-by-side"
```

The command can also be set through the `HELM_DIFF_TOOL` environment variable, which is convenient in a shell profile:

```shell
export HELM_DIFF_TOOL="difft --language yaml"
helm diff upgrade api ./charts/api
```

`--diff-tool` takes precedence over `HELM_DIFF_TOOL`, and either one overrides `--output`. There is no default command: without one, the built-in `--output` renderer is used.

Notes:

- The command is executed directly, not through a shell, so pipes and shell expansion are not available. Wrap arguments containing spaces in quotes, for example `--diff-tool '"/opt/my tools/diff" -u'`. For anything more involved, point the flag at a wrapper script.
- The manifests handed to the tool are the ones from the diff report, so `--suppress`, `--suppress-output-line-regex` and secret redaction still apply. Secrets are redacted unless `--show-secrets` is given, and suppressed kinds are replaced by a placeholder on both sides.
- An exit code of `1` from the tool is treated as "differences found" and ignored. Other failures are reported on stderr without aborting helm-diff.
- helm-diff's own exit code is unaffected by the tool: `--detailed-exitcode` still returns `2` based on the changes helm-diff detected.
- `--context`/`-C` is not applied; use the equivalent option of the external tool (for example `diff -U3`).

## Commands:

### local:
Expand Down Expand Up @@ -248,6 +278,7 @@ Flags:
-a, --api-versions stringArray Kubernetes api versions used for Capabilities.APIVersions
-C, --context int output NUM lines of context around changes (default -1)
--detailed-exitcode return a non-zero exit code when there are changes
--diff-tool string command used to compare the manifests instead of the built-in --output renderers (can also be set via the env var HELM_DIFF_TOOL). The old and the new manifest file paths are appended as the last two arguments
--enable-dns enable DNS lookups when rendering templates
-D, --find-renames float32 Enable rename detection if set to any value greater than 0. If specified, the value denotes the maximum fraction of changed content as lines added + removed compared to total lines in a diff for considering it a rename. Only objects of the same Kind are attempted to be matched
-h, --help help for local
Expand Down Expand Up @@ -328,6 +359,7 @@ Flags:
-C, --context int output NUM lines of context around changes (default -1)
--detailed-exitcode return a non-zero exit code when there are changes
--devel use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored.
--diff-tool string command used to compare the manifests instead of the built-in --output renderers (can also be set via the env var HELM_DIFF_TOOL). The old and the new manifest file paths are appended as the last two arguments
--disable-openapi-validation disables rendered templates validation against the Kubernetes OpenAPI Schema
--disable-validation disables rendered templates validation against the Kubernetes cluster you are currently pointing to. This is the same validation performed on an install
--dry-run string[="client"] --dry-run, --dry-run=client, or --dry-run=true disables cluster access and show diff as if it was install. Implies --install, --reset-values, and --disable-validation. --dry-run=server enables the cluster access with helm-get and the lookup template function.
Expand Down Expand Up @@ -394,6 +426,7 @@ Usage:
Flags:
-C, --context int output NUM lines of context around changes (default -1)
--detailed-exitcode return a non-zero exit code when there are changes
--diff-tool string command used to compare the manifests instead of the built-in --output renderers (can also be set via the env var HELM_DIFF_TOOL). The old and the new manifest file paths are appended as the last two arguments
-D, --find-renames float32 Enable rename detection if set to any value greater than 0. If specified, the value denotes the maximum fraction of changed content as lines added + removed compared to total lines in a diff for considering it a rename. Only objects of the same Kind are attempted to be matched
-h, --help help for release
--include-tests enable the diffing of the helm test hooks
Expand Down Expand Up @@ -436,6 +469,7 @@ Flags:
-C, --context int output NUM lines of context around changes (default -1)
--show-secrets-decoded decode secret values in the output
--detailed-exitcode return a non-zero exit code when there are changes
--diff-tool string command used to compare the manifests instead of the built-in --output renderers (can also be set via the env var HELM_DIFF_TOOL). The old and the new manifest file paths are appended as the last two arguments
-D, --find-renames float32 Enable rename detection if set to any value greater than 0. If specified, the value denotes the maximum fraction of changed content as lines added + removed compared to total lines in a diff for considering it a rename. Only objects of the same Kind are attempted to be matched
-h, --help help for revision
--include-tests enable the diffing of the helm test hooks
Expand Down Expand Up @@ -472,6 +506,7 @@ Examples:
Flags:
-C, --context int output NUM lines of context around changes (default -1)
--detailed-exitcode return a non-zero exit code when there are changes
--diff-tool string command used to compare the manifests instead of the built-in --output renderers (can also be set via the env var HELM_DIFF_TOOL). The old and the new manifest file paths are appended as the last two arguments
-D, --find-renames float32 Enable rename detection if set to any value greater than 0. If specified, the value denotes the maximum fraction of changed content as lines added + removed compared to total lines in a diff for considering it a rename. Only objects of the same Kind are attempted to be matched
-h, --help help for rollback
--include-tests enable the diffing of the helm test hooks
Expand Down
1 change: 1 addition & 0 deletions cmd/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ func AddDiffOptions(f *pflag.FlagSet, o *diff.Options) {
f.StringArrayVar(&o.SuppressedKinds, "suppress", []string{}, "allows suppression of the kinds listed in the diff output (can specify multiple, like '--suppress Deployment --suppress Service')")
f.IntVarP(&o.OutputContext, "context", "C", -1, "output NUM lines of context around changes")
f.StringVar(&o.OutputFormat, "output", "diff", "Possible values: diff, simple, template, json, structured, dyff. When set to \"template\", use the env var HELM_DIFF_TPL to specify the template.")
f.StringVar(&o.DiffToolCommand, "diff-tool", "", "command used to compare the manifests instead of the built-in --output renderers (can also be set via the env var HELM_DIFF_TOOL). The old and the new manifest file paths are appended as the last two arguments")
Comment thread
eagafonov marked this conversation as resolved.
f.BoolVar(&o.StripTrailingCR, "strip-trailing-cr", false, "strip trailing carriage return on input")
f.Float32VarP(&o.FindRenames, "find-renames", "D", 0, "Enable rename detection if set to any value greater than 0. If specified, the value denotes the maximum fraction of changed content as lines added + removed compared to total lines in a diff for considering it a rename. Only objects of the same Kind are attempted to be matched")
f.StringArrayVar(&o.SuppressedOutputLineRegex, "suppress-output-line-regex", []string{}, "a regex to suppress diff output lines that match")
Expand Down
72 changes: 72 additions & 0 deletions cmd/options_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package cmd

import (
"testing"

"github.com/spf13/pflag"
"github.com/stretchr/testify/require"

"github.com/databus23/helm-diff/v3/diff"
)

func processedOptions(t *testing.T, args ...string) diff.Options {
t.Helper()

var o diff.Options
f := pflag.NewFlagSet("test", pflag.ContinueOnError)
AddDiffOptions(f, &o)
require.NoError(t, f.Parse(args))
ProcessDiffOptions(f, &o)

return o
}

func TestProcessDiffOptionsSuppressSecrets(t *testing.T) {
o := processedOptions(t, "--suppress-secrets")
require.Contains(t, o.SuppressedKinds, "Secret")
}

func TestAddDiffOptionsHasNoExternalOutputFormat(t *testing.T) {
var o diff.Options
f := pflag.NewFlagSet("test", pflag.ContinueOnError)
AddDiffOptions(f, &o)

require.NotContains(t, f.Lookup("output").Usage, "external",
"--diff-tool is the only way to select an external tool")
}

func TestProcessDiffOptionsDiffTool(t *testing.T) {
t.Run("no external diff by default", func(t *testing.T) {
t.Setenv(diff.DiffToolEnvVar, "")
o := processedOptions(t)
require.Equal(t, "diff", o.OutputFormat)
require.Empty(t, o.DiffToolCommand)
require.False(t, o.DiffTool())
})

t.Run("--diff-tool enables the external tool", func(t *testing.T) {
t.Setenv(diff.DiffToolEnvVar, "")
o := processedOptions(t, "--diff-tool", "difft")
require.Equal(t, "difft", o.DiffToolCommand)
require.True(t, o.DiffTool())
})

t.Run("HELM_DIFF_TOOL enables the external tool", func(t *testing.T) {
t.Setenv(diff.DiffToolEnvVar, "colordiff -u")
o := processedOptions(t)
require.True(t, o.DiffTool())
})

t.Run("the external tool overrides an explicit --output", func(t *testing.T) {
t.Setenv(diff.DiffToolEnvVar, "")
o := processedOptions(t, "--diff-tool", "difft", "--output", "json")
require.Equal(t, "json", o.OutputFormat, "--output keeps its value")
require.True(t, o.DiffTool(), "but the external tool takes precedence")
})

t.Run("an empty --diff-tool keeps the built-in output", func(t *testing.T) {
t.Setenv(diff.DiffToolEnvVar, "")
o := processedOptions(t, "--diff-tool", "", "--output", "simple")
require.False(t, o.DiffTool())
})
}
25 changes: 20 additions & 5 deletions diff/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,22 @@
SuppressedKinds []string
FindRenames float32
SuppressedOutputLineRegex []string
DiffToolCommand string
}

const kindSecret = "Secret"

// StructuredOutput returns true when the structured JSON output is requested.
// StructuredOutput returns true when the structured JSON output is requested
// except when using a diff tool, whose input is the line diffs that structured
// output skips.
func (o *Options) StructuredOutput() bool {
return o != nil && o.OutputFormat == "structured"
return o != nil && o.OutputFormat == "structured" && !o.DiffTool()

Check failure on line 42 in diff/diff.go

View workflow job for this annotation

GitHub Actions / Lint

string `structured` has 9 occurrences, make it a constant (goconst)
}

// DiffTool reports whether the diff is rendered by an external tool. Configuring a
// command is the only way to ask for it, and it overrides the built-in outputs.
func (o *Options) DiffTool() bool {
return o != nil && diffToolCommand(o.DiffToolCommand) != ""
}

type OwnershipDiff struct {
Expand Down Expand Up @@ -67,8 +76,13 @@
}

func generateReport(oldIndex, newIndex map[string]*manifest.MappingResult, newOwnedReleases map[string]OwnershipDiff, options *Options) (bool, *Report, error) {
report := Report{findRenames: options.FindRenames}
report.setupReportFormat(options.OutputFormat)
report := Report{findRenames: options.FindRenames, diffToolCommand: options.DiffToolCommand}
if options.DiffTool() {
// A configured diff tool replaces whatever built-in output was selected.
setupDiffToolReport(&report)
} else {
report.setupReportFormat(options.OutputFormat)
}
var possiblyRemoved []string

for name, diff := range newOwnedReleases {
Expand Down Expand Up @@ -121,7 +135,8 @@
}

filteredReport := Report{
findRenames: report.findRenames,
findRenames: report.findRenames,
diffToolCommand: report.diffToolCommand,
}
filteredReport.format = report.format
filteredReport.Entries = []ReportEntry{}
Expand Down Expand Up @@ -165,7 +180,7 @@
switch {
case containsDiff:
diffRecords = diffs
case entry.ChangeType == "MODIFY":

Check failure on line 183 in diff/diff.go

View workflow job for this annotation

GitHub Actions / Lint

string `MODIFY` has 13 occurrences, make it a constant (goconst)
entry.ChangeType = "MODIFY_SUPPRESSED"
}

Expand Down
Loading
Loading