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
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ Usage:

Available Commands:
completion Generate the autocompletion script for the specified shell
local Shows diff between two local chart directories
release Shows diff between release's manifests
revision Shows diff between revision's manifests
rollback Show a diff explaining what a helm rollback could perform
Expand Down Expand Up @@ -134,6 +135,63 @@ Use "diff [command] --help" for more information about a command.

## Commands:

### local:

```
$ helm diff local -h

This command compares the manifests of two local chart directories.

It renders both charts using 'helm template' and shows the differences
between the resulting manifests.

This is useful for:
- Comparing different versions of a chart
- Previewing changes before committing
- Validating chart modifications

Usage:
diff local [flags] CHART1 CHART2

Examples:
helm diff local ./chart-v1 ./chart-v2
helm diff local ./chart-v1 ./chart-v2 -f values.yaml
helm diff local /path/to/chart-a /path/to/chart-b --set replicas=3

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
--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
--include-crds include CRDs in the diffing
--include-tests enable the diffing of the helm test hooks
--kube-version string Kubernetes version used for Capabilities.KubeVersion
--namespace string namespace to use for template rendering
--normalize-manifests normalize manifests before running diff to exclude style differences from the output
--output string Possible values: diff, simple, template, dyff. When set to "template", use the env var HELM_DIFF_TPL to specify the template. (default "diff")
--post-renderer string the path to an executable to be used for post rendering. If it exists in $PATH, the binary will be used, otherwise it will try to look for the executable at the given path
--post-renderer-args stringArray an argument to the post-renderer (can specify multiple)
--release string release name to use for template rendering (default "release")
--set stringArray set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)
--set-file stringArray set values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2)
--set-json stringArray set JSON values on the command line (can specify multiple or separate values with commas: key1=jsonval1,key2=jsonval2)
--set-literal stringArray set STRING literal values on the command line
--set-string stringArray set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)
--show-secrets do not redact secret values in the output
--show-secrets-decoded decode secret values in the output
--strip-trailing-cr strip trailing carriage return on input
--suppress stringArray allows suppression of the kinds listed in the diff output (can specify multiple, like '--suppress Deployment --suppress Service')
--suppress-output-line-regex stringArray a regex to suppress diff output lines that match
-q, --suppress-secrets suppress secrets in the output
-f, --values valueFiles specify values in a YAML file (can specify multiple) (default [])

Global Flags:
--color color output. You can control the value for this flag via HELM_DIFF_COLOR=[true|false]. If both --no-color and --color are unspecified, coloring enabled only when the stdout is a term and TERM is not "dumb"
--no-color remove colors from the output. If both --no-color and --color are unspecified, coloring enabled only when the stdout is a term and TERM is not "dumb"
```

### upgrade:

```
Expand Down
234 changes: 234 additions & 0 deletions cmd/local.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
package cmd

import (
"errors"
"fmt"
"io"
"os"
"os/exec"
"strings"

"github.com/spf13/cobra"

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

type local struct {
chart1 string
chart2 string
release string
namespace string
detailedExitCode bool
includeTests bool
includeCRDs bool
normalizeManifests bool
enableDNS bool
valueFiles valueFiles
values []string
stringValues []string
stringLiteralValues []string
jsonValues []string
fileValues []string
postRenderer string
postRendererArgs []string
extraAPIs []string
kubeVersion string
diff.Options
}

const localCmdLongUsage = `
This command compares the manifests of two local chart directories.
It renders both charts using 'helm template' and shows the differences
between the resulting manifests.
This is useful for:
- Comparing different versions of a chart
- Previewing changes before committing
- Validating chart modifications
`

func localCmd() *cobra.Command {
diff := local{
release: "release",
}

localCmd := &cobra.Command{
Use: "local [flags] CHART1 CHART2",
Short: "Shows diff between two local chart directories",
Long: localCmdLongUsage,
Example: strings.Join([]string{
" helm diff local ./chart-v1 ./chart-v2",
" helm diff local ./chart-v1 ./chart-v2 -f values.yaml",
" helm diff local /path/to/chart-a /path/to/chart-b --set replicas=3",
}, "\n"),
RunE: func(cmd *cobra.Command, args []string) error {
// Suppress the command usage on error. See #77 for more info
cmd.SilenceUsage = true

if v, _ := cmd.Flags().GetBool("version"); v {
fmt.Println(Version)
return nil
}

if err := checkArgsLength(len(args), "chart1 path", "chart2 path"); err != nil {
return err
}

ProcessDiffOptions(cmd.Flags(), &diff.Options)

diff.chart1 = args[0]
diff.chart2 = args[1]

if diff.namespace == "" {
diff.namespace = os.Getenv("HELM_NAMESPACE")
}

return diff.run()
},
}

localCmd.Flags().StringVar(&diff.release, "release", "release", "release name to use for template rendering")
localCmd.Flags().StringVar(&diff.namespace, "namespace", "", "namespace to use for template rendering")
localCmd.Flags().BoolVar(&diff.detailedExitCode, "detailed-exitcode", false, "return a non-zero exit code when there are changes")
localCmd.Flags().BoolVar(&diff.includeTests, "include-tests", false, "enable the diffing of the helm test hooks")
localCmd.Flags().BoolVar(&diff.includeCRDs, "include-crds", false, "include CRDs in the diffing")
localCmd.Flags().BoolVar(&diff.normalizeManifests, "normalize-manifests", false, "normalize manifests before running diff to exclude style differences from the output")
localCmd.Flags().BoolVar(&diff.enableDNS, "enable-dns", false, "enable DNS lookups when rendering templates")
localCmd.Flags().VarP(&diff.valueFiles, "values", "f", "specify values in a YAML file (can specify multiple)")
localCmd.Flags().StringArrayVar(&diff.values, "set", []string{}, "set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)")
localCmd.Flags().StringArrayVar(&diff.stringValues, "set-string", []string{}, "set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)")
localCmd.Flags().StringArrayVar(&diff.stringLiteralValues, "set-literal", []string{}, "set STRING literal values on the command line")
localCmd.Flags().StringArrayVar(&diff.jsonValues, "set-json", []string{}, "set JSON values on the command line (can specify multiple or separate values with commas: key1=jsonval1,key2=jsonval2)")
localCmd.Flags().StringArrayVar(&diff.fileValues, "set-file", []string{}, "set values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2)")
localCmd.Flags().StringVar(&diff.postRenderer, "post-renderer", "", "the path to an executable to be used for post rendering. If it exists in $PATH, the binary will be used, otherwise it will try to look for the executable at the given path")
localCmd.Flags().StringArrayVar(&diff.postRendererArgs, "post-renderer-args", []string{}, "an argument to the post-renderer (can specify multiple)")
localCmd.Flags().StringArrayVarP(&diff.extraAPIs, "api-versions", "a", []string{}, "Kubernetes api versions used for Capabilities.APIVersions")
localCmd.Flags().StringVar(&diff.kubeVersion, "kube-version", "", "Kubernetes version used for Capabilities.KubeVersion")

AddDiffOptions(localCmd.Flags(), &diff.Options)

localCmd.SuggestionsMinimumDistance = 1

return localCmd
}

func (l *local) run() error {
manifest1, err := l.renderChart(l.chart1)
if err != nil {
return fmt.Errorf("Failed to render chart %s: %w", l.chart1, err)
Copy link

Copilot AI Oct 10, 2025

Choose a reason for hiding this comment

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

Error messages should start with lowercase letters to follow Go conventions.

Suggested change
return fmt.Errorf("Failed to render chart %s: %w", l.chart1, err)
return fmt.Errorf("failed to render chart %s: %w", l.chart1, err)

Copilot uses AI. Check for mistakes.

}

manifest2, err := l.renderChart(l.chart2)
if err != nil {
return fmt.Errorf("Failed to render chart %s: %w", l.chart2, err)
Copy link

Copilot AI Oct 10, 2025

Choose a reason for hiding this comment

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

Error messages should start with lowercase letters to follow Go conventions.

Suggested change
return fmt.Errorf("Failed to render chart %s: %w", l.chart2, err)
return fmt.Errorf("failed to render chart %s: %w", l.chart2, err)

Copilot uses AI. Check for mistakes.

}

excludes := []string{manifest.Helm3TestHook, manifest.Helm2TestSuccessHook}
if l.includeTests {
excludes = []string{}
}

specs1 := manifest.Parse(string(manifest1), l.namespace, l.normalizeManifests, excludes...)
specs2 := manifest.Parse(string(manifest2), l.namespace, l.normalizeManifests, excludes...)

seenAnyChanges := diff.Manifests(specs1, specs2, &l.Options, os.Stdout)

if l.detailedExitCode && seenAnyChanges {
return Error{
error: errors.New("identified at least one change, exiting with non-zero exit code (detailed-exitcode parameter enabled)"),
Code: 2,
}
}

return nil
}

func (l *local) renderChart(chartPath string) ([]byte, error) {
flags := []string{}

if l.includeCRDs {
flags = append(flags, "--include-crds")
}

if l.namespace != "" {
flags = append(flags, "--namespace", l.namespace)
}

if l.postRenderer != "" {
flags = append(flags, "--post-renderer", l.postRenderer)
}

for _, arg := range l.postRendererArgs {
flags = append(flags, "--post-renderer-args", arg)
}

for _, valueFile := range l.valueFiles {
if strings.TrimSpace(valueFile) == "-" {
bytes, err := io.ReadAll(os.Stdin)
if err != nil {
return nil, err
}

tmpfile, err := os.CreateTemp("", "helm-diff-stdin-values")
if err != nil {
return nil, err
}
defer func() {
_ = os.Remove(tmpfile.Name())
}()

if _, err := tmpfile.Write(bytes); err != nil {
_ = tmpfile.Close()
return nil, err
}

if err := tmpfile.Close(); err != nil {
return nil, err
}

flags = append(flags, "--values", tmpfile.Name())
} else {
flags = append(flags, "--values", valueFile)
}
}

for _, value := range l.values {
flags = append(flags, "--set", value)
}

for _, stringValue := range l.stringValues {
flags = append(flags, "--set-string", stringValue)
}

for _, stringLiteralValue := range l.stringLiteralValues {
flags = append(flags, "--set-literal", stringLiteralValue)
}

for _, jsonValue := range l.jsonValues {
flags = append(flags, "--set-json", jsonValue)
}

for _, fileValue := range l.fileValues {
flags = append(flags, "--set-file", fileValue)
}

if l.enableDNS {
flags = append(flags, "--enable-dns")
}

for _, a := range l.extraAPIs {
flags = append(flags, "--api-versions", a)
}

if l.kubeVersion != "" {
flags = append(flags, "--kube-version", l.kubeVersion)
}

args := []string{"template", l.release, chartPath}
args = append(args, flags...)

cmd := exec.Command(os.Getenv("HELM_BIN"), args...)
Copy link

Copilot AI Oct 10, 2025

Choose a reason for hiding this comment

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

Using os.Getenv("HELM_BIN") without validation could allow execution of arbitrary commands. Consider validating the HELM_BIN path or providing a default fallback.

Suggested change
cmd := exec.Command(os.Getenv("HELM_BIN"), args...)
helmBin := os.Getenv("HELM_BIN")
if helmBin == "" {
helmBin = "helm"
}
cmd := exec.Command(helmBin, args...)

Copilot uses AI. Check for mistakes.

return outputWithRichError(cmd)
}
81 changes: 81 additions & 0 deletions cmd/local_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package cmd

import (
"os"
"testing"
)

func TestLocalCmdArgValidation(t *testing.T) {
cases := []struct {
name string
args []string
expectError bool
}{
{
name: "no arguments",
args: []string{},
expectError: true,
},
{
name: "one argument",
args: []string{"chart1"},
expectError: true,
},
{
name: "three arguments",
args: []string{"chart1", "chart2", "chart3"},
expectError: true,
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
cmd := localCmd()
cmd.SetArgs(tc.args)
err := cmd.Execute()

if tc.expectError && err == nil {
t.Errorf("Expected error but got none")
}
if !tc.expectError && err != nil {
t.Errorf("Expected no error but got: %v", err)
}
})
}
}

func TestLocalCmdExecution(t *testing.T) {
tmpDir := t.TempDir()
fakeHelm := tmpDir + "/helm"
manifestYAML := `---
apiVersion: v1
kind: ConfigMap
metadata:
name: test-config
namespace: default
data:
key: value
`

err := os.WriteFile(fakeHelm, []byte(`#!/bin/sh
cat <<EOF
`+manifestYAML+`
EOF
`), 0755)
if err != nil {
t.Fatal(err)
}

t.Setenv("HELM_BIN", fakeHelm)

chart1 := t.TempDir()
chart2 := t.TempDir()

cmd := localCmd()
cmd.SetArgs([]string{chart1, chart2})

err = cmd.Execute()
if err != nil {
t.Errorf("Expected no error but got: %v", err)
}
}
1 change: 1 addition & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ func New() *cobra.Command {
revisionCmd(),
rollbackCmd(),
releaseCmd(),
localCmd(),
)
cmd.SetHelpCommand(&cobra.Command{}) // Disable the help command
return cmd
Expand Down
Loading