Skip to content

Commit d5e0210

Browse files
committed
feat: add local subcommand to compare two chart folders
1 parent 2949077 commit d5e0210

File tree

4 files changed

+375
-0
lines changed

4 files changed

+375
-0
lines changed

README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ Usage:
7474
7575
Available Commands:
7676
completion Generate the autocompletion script for the specified shell
77+
local Shows diff between two local chart directories
7778
release Shows diff between release's manifests
7879
revision Shows diff between revision's manifests
7980
rollback Show a diff explaining what a helm rollback could perform
@@ -134,6 +135,63 @@ Use "diff [command] --help" for more information about a command.
134135

135136
## Commands:
136137

138+
### local:
139+
140+
```
141+
$ helm diff local -h
142+
143+
This command compares the manifests of two local chart directories.
144+
145+
It renders both charts using 'helm template' and shows the differences
146+
between the resulting manifests.
147+
148+
This is useful for:
149+
- Comparing different versions of a chart
150+
- Previewing changes before committing
151+
- Validating chart modifications
152+
153+
Usage:
154+
diff local [flags] CHART1 CHART2
155+
156+
Examples:
157+
helm diff local ./chart-v1 ./chart-v2
158+
helm diff local ./chart-v1 ./chart-v2 -f values.yaml
159+
helm diff local /path/to/chart-a /path/to/chart-b --set replicas=3
160+
161+
Flags:
162+
-a, --api-versions stringArray Kubernetes api versions used for Capabilities.APIVersions
163+
-C, --context int output NUM lines of context around changes (default -1)
164+
--detailed-exitcode return a non-zero exit code when there are changes
165+
--enable-dns enable DNS lookups when rendering templates
166+
-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
167+
-h, --help help for local
168+
--include-crds include CRDs in the diffing
169+
--include-tests enable the diffing of the helm test hooks
170+
--kube-version string Kubernetes version used for Capabilities.KubeVersion
171+
--namespace string namespace to use for template rendering
172+
--normalize-manifests normalize manifests before running diff to exclude style differences from the output
173+
--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")
174+
--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
175+
--post-renderer-args stringArray an argument to the post-renderer (can specify multiple)
176+
--release string release name to use for template rendering (default "release")
177+
--set stringArray set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)
178+
--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)
179+
--set-json stringArray set JSON values on the command line (can specify multiple or separate values with commas: key1=jsonval1,key2=jsonval2)
180+
--set-literal stringArray set STRING literal values on the command line
181+
--set-string stringArray set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)
182+
--show-secrets do not redact secret values in the output
183+
--show-secrets-decoded decode secret values in the output
184+
--strip-trailing-cr strip trailing carriage return on input
185+
--suppress stringArray allows suppression of the kinds listed in the diff output (can specify multiple, like '--suppress Deployment --suppress Service')
186+
--suppress-output-line-regex stringArray a regex to suppress diff output lines that match
187+
-q, --suppress-secrets suppress secrets in the output
188+
-f, --values valueFiles specify values in a YAML file (can specify multiple) (default [])
189+
190+
Global Flags:
191+
--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"
192+
--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"
193+
```
194+
137195
### upgrade:
138196

139197
```

cmd/local.go

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
package cmd
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"io"
7+
"os"
8+
"os/exec"
9+
"strings"
10+
11+
"github.com/spf13/cobra"
12+
13+
"github.com/databus23/helm-diff/v3/diff"
14+
"github.com/databus23/helm-diff/v3/manifest"
15+
)
16+
17+
type local struct {
18+
chart1 string
19+
chart2 string
20+
release string
21+
namespace string
22+
detailedExitCode bool
23+
includeTests bool
24+
includeCRDs bool
25+
normalizeManifests bool
26+
enableDNS bool
27+
valueFiles valueFiles
28+
values []string
29+
stringValues []string
30+
stringLiteralValues []string
31+
jsonValues []string
32+
fileValues []string
33+
postRenderer string
34+
postRendererArgs []string
35+
extraAPIs []string
36+
kubeVersion string
37+
diff.Options
38+
}
39+
40+
const localCmdLongUsage = `
41+
This command compares the manifests of two local chart directories.
42+
43+
It renders both charts using 'helm template' and shows the differences
44+
between the resulting manifests.
45+
46+
This is useful for:
47+
- Comparing different versions of a chart
48+
- Previewing changes before committing
49+
- Validating chart modifications
50+
`
51+
52+
func localCmd() *cobra.Command {
53+
diff := local{
54+
release: "release",
55+
}
56+
57+
localCmd := &cobra.Command{
58+
Use: "local [flags] CHART1 CHART2",
59+
Short: "Shows diff between two local chart directories",
60+
Long: localCmdLongUsage,
61+
Example: strings.Join([]string{
62+
" helm diff local ./chart-v1 ./chart-v2",
63+
" helm diff local ./chart-v1 ./chart-v2 -f values.yaml",
64+
" helm diff local /path/to/chart-a /path/to/chart-b --set replicas=3",
65+
}, "\n"),
66+
RunE: func(cmd *cobra.Command, args []string) error {
67+
// Suppress the command usage on error. See #77 for more info
68+
cmd.SilenceUsage = true
69+
70+
if v, _ := cmd.Flags().GetBool("version"); v {
71+
fmt.Println(Version)
72+
return nil
73+
}
74+
75+
if err := checkArgsLength(len(args), "chart1 path", "chart2 path"); err != nil {
76+
return err
77+
}
78+
79+
ProcessDiffOptions(cmd.Flags(), &diff.Options)
80+
81+
diff.chart1 = args[0]
82+
diff.chart2 = args[1]
83+
84+
if diff.namespace == "" {
85+
diff.namespace = os.Getenv("HELM_NAMESPACE")
86+
}
87+
88+
return diff.run()
89+
},
90+
}
91+
92+
localCmd.Flags().StringVar(&diff.release, "release", "release", "release name to use for template rendering")
93+
localCmd.Flags().StringVar(&diff.namespace, "namespace", "", "namespace to use for template rendering")
94+
localCmd.Flags().BoolVar(&diff.detailedExitCode, "detailed-exitcode", false, "return a non-zero exit code when there are changes")
95+
localCmd.Flags().BoolVar(&diff.includeTests, "include-tests", false, "enable the diffing of the helm test hooks")
96+
localCmd.Flags().BoolVar(&diff.includeCRDs, "include-crds", false, "include CRDs in the diffing")
97+
localCmd.Flags().BoolVar(&diff.normalizeManifests, "normalize-manifests", false, "normalize manifests before running diff to exclude style differences from the output")
98+
localCmd.Flags().BoolVar(&diff.enableDNS, "enable-dns", false, "enable DNS lookups when rendering templates")
99+
localCmd.Flags().VarP(&diff.valueFiles, "values", "f", "specify values in a YAML file (can specify multiple)")
100+
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)")
101+
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)")
102+
localCmd.Flags().StringArrayVar(&diff.stringLiteralValues, "set-literal", []string{}, "set STRING literal values on the command line")
103+
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)")
104+
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)")
105+
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")
106+
localCmd.Flags().StringArrayVar(&diff.postRendererArgs, "post-renderer-args", []string{}, "an argument to the post-renderer (can specify multiple)")
107+
localCmd.Flags().StringArrayVarP(&diff.extraAPIs, "api-versions", "a", []string{}, "Kubernetes api versions used for Capabilities.APIVersions")
108+
localCmd.Flags().StringVar(&diff.kubeVersion, "kube-version", "", "Kubernetes version used for Capabilities.KubeVersion")
109+
110+
AddDiffOptions(localCmd.Flags(), &diff.Options)
111+
112+
localCmd.SuggestionsMinimumDistance = 1
113+
114+
return localCmd
115+
}
116+
117+
func (l *local) run() error {
118+
manifest1, err := l.renderChart(l.chart1)
119+
if err != nil {
120+
return fmt.Errorf("Failed to render chart %s: %w", l.chart1, err)
121+
}
122+
123+
manifest2, err := l.renderChart(l.chart2)
124+
if err != nil {
125+
return fmt.Errorf("Failed to render chart %s: %w", l.chart2, err)
126+
}
127+
128+
excludes := []string{manifest.Helm3TestHook, manifest.Helm2TestSuccessHook}
129+
if l.includeTests {
130+
excludes = []string{}
131+
}
132+
133+
specs1 := manifest.Parse(string(manifest1), l.namespace, l.normalizeManifests, excludes...)
134+
specs2 := manifest.Parse(string(manifest2), l.namespace, l.normalizeManifests, excludes...)
135+
136+
seenAnyChanges := diff.Manifests(specs1, specs2, &l.Options, os.Stdout)
137+
138+
if l.detailedExitCode && seenAnyChanges {
139+
return Error{
140+
error: errors.New("identified at least one change, exiting with non-zero exit code (detailed-exitcode parameter enabled)"),
141+
Code: 2,
142+
}
143+
}
144+
145+
return nil
146+
}
147+
148+
func (l *local) renderChart(chartPath string) ([]byte, error) {
149+
flags := []string{}
150+
151+
if l.includeCRDs {
152+
flags = append(flags, "--include-crds")
153+
}
154+
155+
if l.namespace != "" {
156+
flags = append(flags, "--namespace", l.namespace)
157+
}
158+
159+
if l.postRenderer != "" {
160+
flags = append(flags, "--post-renderer", l.postRenderer)
161+
}
162+
163+
for _, arg := range l.postRendererArgs {
164+
flags = append(flags, "--post-renderer-args", arg)
165+
}
166+
167+
for _, valueFile := range l.valueFiles {
168+
if strings.TrimSpace(valueFile) == "-" {
169+
bytes, err := io.ReadAll(os.Stdin)
170+
if err != nil {
171+
return nil, err
172+
}
173+
174+
tmpfile, err := os.CreateTemp("", "helm-diff-stdin-values")
175+
if err != nil {
176+
return nil, err
177+
}
178+
defer func() {
179+
_ = os.Remove(tmpfile.Name())
180+
}()
181+
182+
if _, err := tmpfile.Write(bytes); err != nil {
183+
_ = tmpfile.Close()
184+
return nil, err
185+
}
186+
187+
if err := tmpfile.Close(); err != nil {
188+
return nil, err
189+
}
190+
191+
flags = append(flags, "--values", tmpfile.Name())
192+
} else {
193+
flags = append(flags, "--values", valueFile)
194+
}
195+
}
196+
197+
for _, value := range l.values {
198+
flags = append(flags, "--set", value)
199+
}
200+
201+
for _, stringValue := range l.stringValues {
202+
flags = append(flags, "--set-string", stringValue)
203+
}
204+
205+
for _, stringLiteralValue := range l.stringLiteralValues {
206+
flags = append(flags, "--set-literal", stringLiteralValue)
207+
}
208+
209+
for _, jsonValue := range l.jsonValues {
210+
flags = append(flags, "--set-json", jsonValue)
211+
}
212+
213+
for _, fileValue := range l.fileValues {
214+
flags = append(flags, "--set-file", fileValue)
215+
}
216+
217+
if l.enableDNS {
218+
flags = append(flags, "--enable-dns")
219+
}
220+
221+
for _, a := range l.extraAPIs {
222+
flags = append(flags, "--api-versions", a)
223+
}
224+
225+
if l.kubeVersion != "" {
226+
flags = append(flags, "--kube-version", l.kubeVersion)
227+
}
228+
229+
args := []string{"template", l.release, chartPath}
230+
args = append(args, flags...)
231+
232+
cmd := exec.Command(os.Getenv("HELM_BIN"), args...)
233+
return outputWithRichError(cmd)
234+
}

cmd/local_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package cmd
2+
3+
import (
4+
"os"
5+
"testing"
6+
)
7+
8+
func TestLocalCmdArgValidation(t *testing.T) {
9+
cases := []struct {
10+
name string
11+
args []string
12+
expectError bool
13+
}{
14+
{
15+
name: "no arguments",
16+
args: []string{},
17+
expectError: true,
18+
},
19+
{
20+
name: "one argument",
21+
args: []string{"chart1"},
22+
expectError: true,
23+
},
24+
{
25+
name: "three arguments",
26+
args: []string{"chart1", "chart2", "chart3"},
27+
expectError: true,
28+
},
29+
}
30+
31+
for _, tc := range cases {
32+
t.Run(tc.name, func(t *testing.T) {
33+
cmd := localCmd()
34+
cmd.SetArgs(tc.args)
35+
err := cmd.Execute()
36+
37+
if tc.expectError && err == nil {
38+
t.Errorf("Expected error but got none")
39+
}
40+
if !tc.expectError && err != nil {
41+
t.Errorf("Expected no error but got: %v", err)
42+
}
43+
})
44+
}
45+
}
46+
47+
func TestLocalCmdExecution(t *testing.T) {
48+
tmpDir := t.TempDir()
49+
fakeHelm := tmpDir + "/helm"
50+
manifestYAML := `---
51+
apiVersion: v1
52+
kind: ConfigMap
53+
metadata:
54+
name: test-config
55+
namespace: default
56+
data:
57+
key: value
58+
`
59+
60+
err := os.WriteFile(fakeHelm, []byte(`#!/bin/sh
61+
cat <<EOF
62+
`+manifestYAML+`
63+
EOF
64+
`), 0755)
65+
if err != nil {
66+
t.Fatal(err)
67+
}
68+
69+
t.Setenv("HELM_BIN", fakeHelm)
70+
71+
chart1 := t.TempDir()
72+
chart2 := t.TempDir()
73+
74+
cmd := localCmd()
75+
cmd.SetArgs([]string{chart1, chart2})
76+
77+
err = cmd.Execute()
78+
if err != nil {
79+
t.Errorf("Expected no error but got: %v", err)
80+
}
81+
}
82+

cmd/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ func New() *cobra.Command {
9494
revisionCmd(),
9595
rollbackCmd(),
9696
releaseCmd(),
97+
localCmd(),
9798
)
9899
cmd.SetHelpCommand(&cobra.Command{}) // Disable the help command
99100
return cmd

0 commit comments

Comments
 (0)