Skip to content

Commit bbf7e68

Browse files
authored
Pass --revision to helm diff upgrade (#1046)
* fix for #1045 * fix lint
1 parent 1dfe2c7 commit bbf7e68

8 files changed

Lines changed: 147 additions & 34 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ Flags:
168168
--reset-then-reuse-values reset the values to the ones built into the chart, apply the last release's values and merge in any new values. If '--reset-values' or '--reuse-values' is specified, this is ignored
169169
--reset-values reset the values to the ones built into the chart and merge in any new values
170170
--reuse-values reuse the last release's values and merge in any new values. If '--reset-values' is specified, this is ignored
171+
--revision int revision of the release to use as the diff baseline instead of the newest one
171172
--server-side string must be "true", "false" or "auto". Object updates run in the server instead of the client ("auto" defaults the value from the previous chart release's method) (default "auto")
172173
--set stringArray set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)
173174
--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)
@@ -349,6 +350,7 @@ Flags:
349350
--reset-then-reuse-values reset the values to the ones built into the chart, apply the last release's values and merge in any new values. If '--reset-values' or '--reuse-values' is specified, this is ignored
350351
--reset-values reset the values to the ones built into the chart and merge in any new values
351352
--reuse-values reuse the last release's values and merge in any new values. If '--reset-values' is specified, this is ignored
353+
--revision int revision of the release to use as the diff baseline instead of the newest one
352354
--server-side string must be "true", "false" or "auto". Object updates run in the server instead of the client ("auto" defaults the value from the previous chart release's method) (default "auto")
353355
--set stringArray set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)
354356
--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)

cmd/helm.go

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -131,44 +131,43 @@ func compatibleHelm3Version() error {
131131
return nil
132132
}
133133

134-
func getRelease(release, namespace, kubeContext string) ([]byte, error) {
135-
args := []string{"get", "manifest", release}
134+
// helmGetSubcmd is the `helm get` subcommand used to read data of a deployed release.
135+
const helmGetSubcmd = "get"
136+
137+
// helmGetArgs builds the arguments for a `helm get <what> <release>` invocation.
138+
//
139+
// A revision of 0 means no --revision flag is passed, so helm defaults to the
140+
// newest revision of the release regardless of its status.
141+
func helmGetArgs(what, release string, revision int, namespace, kubeContext string) []string {
142+
args := []string{helmGetSubcmd, what, release}
143+
if revision > 0 {
144+
args = append(args, "--revision", strconv.Itoa(revision))
145+
}
136146
if namespace != "" {
137147
args = append(args, "--namespace", namespace)
138148
}
139149
if kubeContext != "" {
140150
args = append(args, "--kube-context", kubeContext)
141151
}
142-
cmd := exec.Command(os.Getenv("HELM_BIN"), args...)
143-
return outputWithRichError(cmd)
152+
return args
144153
}
145154

146-
func getHooks(release, namespace, kubeContext string) ([]byte, error) {
147-
args := []string{"get", "hooks", release}
148-
if namespace != "" {
149-
args = append(args, "--namespace", namespace)
150-
}
151-
if kubeContext != "" {
152-
args = append(args, "--kube-context", kubeContext)
153-
}
154-
cmd := exec.Command(os.Getenv("HELM_BIN"), args...)
155+
// getRelease returns the manifest of the given release revision.
156+
// A revision of 0 means the newest revision.
157+
func getRelease(release string, revision int, namespace, kubeContext string) ([]byte, error) {
158+
cmd := exec.Command(os.Getenv("HELM_BIN"), helmGetArgs("manifest", release, revision, namespace, kubeContext)...)
155159
return outputWithRichError(cmd)
156160
}
157161

158-
func getRevision(release string, revision int, namespace, kubeContext string) ([]byte, error) {
159-
args := []string{"get", "manifest", release, "--revision", strconv.Itoa(revision)}
160-
if namespace != "" {
161-
args = append(args, "--namespace", namespace)
162-
}
163-
if kubeContext != "" {
164-
args = append(args, "--kube-context", kubeContext)
165-
}
166-
cmd := exec.Command(os.Getenv("HELM_BIN"), args...)
162+
// getHooks returns the hooks of the given release revision.
163+
// A revision of 0 means the newest revision.
164+
func getHooks(release string, revision int, namespace, kubeContext string) ([]byte, error) {
165+
cmd := exec.Command(os.Getenv("HELM_BIN"), helmGetArgs("hooks", release, revision, namespace, kubeContext)...)
167166
return outputWithRichError(cmd)
168167
}
169168

170169
func getChart(release, namespace, kubeContext string) (string, error) {
171-
args := []string{"get", "all", release, "--template", "{{.Release.Chart.Name}}"}
170+
args := []string{helmGetSubcmd, "all", release, "--template", "{{.Release.Chart.Name}}"}
172171
if namespace != "" {
173172
args = append(args, "--namespace", namespace)
174173
}
@@ -425,7 +424,7 @@ func (d *diffCmd) template(isUpgrade bool) ([]byte, error) {
425424
}
426425

427426
func (d *diffCmd) writeExistingValues(f *os.File, all bool) error {
428-
args := []string{"get", "values", d.release, "--output", "yaml"}
427+
args := []string{helmGetSubcmd, "values", d.release, "--output", "yaml"}
429428
if all {
430429
args = append(args, "--all")
431430
}

cmd/helm_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,3 +522,63 @@ To connect to your database directly from outside the K8s cluster:
522522
})
523523
}
524524
}
525+
526+
func TestHelmGetArgs(t *testing.T) {
527+
cases := []struct {
528+
name string
529+
what string
530+
release string
531+
revision int
532+
namespace string
533+
kubeContext string
534+
expected []string
535+
}{
536+
{
537+
name: "manifest without revision omits the flag",
538+
what: "manifest",
539+
release: "myapp",
540+
revision: 0,
541+
expected: []string{helmGetSubcmd, "manifest", "myapp"},
542+
},
543+
{
544+
name: "manifest with revision",
545+
what: "manifest",
546+
release: "myapp",
547+
revision: 49,
548+
expected: []string{helmGetSubcmd, "manifest", "myapp", "--revision", "49"},
549+
},
550+
{
551+
name: "hooks with revision",
552+
what: "hooks",
553+
release: "myapp",
554+
revision: 49,
555+
expected: []string{helmGetSubcmd, "hooks", "myapp", "--revision", "49"},
556+
},
557+
{
558+
name: "revision with namespace and kube context",
559+
what: "manifest",
560+
release: "myapp",
561+
revision: 2,
562+
namespace: "myns",
563+
kubeContext: "myctx",
564+
expected: []string{helmGetSubcmd, "manifest", "myapp", "--revision", "2", "--namespace", "myns", "--kube-context", "myctx"},
565+
},
566+
{
567+
name: "negative revision is treated as unset",
568+
what: "manifest",
569+
release: "myapp",
570+
revision: -1,
571+
namespace: "myns",
572+
expected: []string{helmGetSubcmd, "manifest", "myapp", "--namespace", "myns"},
573+
},
574+
}
575+
576+
for _, tc := range cases {
577+
t.Run(tc.name, func(t *testing.T) {
578+
actual := helmGetArgs(tc.what, tc.release, tc.revision, tc.namespace, tc.kubeContext)
579+
if d := cmp.Diff(tc.expected, actual); d != "" {
580+
t.Errorf("unexpected diff: %s", d)
581+
}
582+
})
583+
}
584+
}

cmd/release.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ func (d *release) differentiateHelm3() error {
8484
namespace1 = strings.Split(release1, "/")[0]
8585
release1 = strings.Split(release1, "/")[1]
8686
}
87-
releaseResponse1, err := getRelease(release1, namespace1, d.kubeContext)
87+
releaseResponse1, err := getRelease(release1, 0, namespace1, d.kubeContext)
8888
if err != nil {
8989
return err
9090
}
@@ -99,7 +99,7 @@ func (d *release) differentiateHelm3() error {
9999
namespace2 = strings.Split(release2, "/")[0]
100100
release2 = strings.Split(release2, "/")[1]
101101
}
102-
releaseResponse2, err := getRelease(release2, namespace2, d.kubeContext)
102+
releaseResponse2, err := getRelease(release2, 0, namespace2, d.kubeContext)
103103
if err != nil {
104104
return err
105105
}

cmd/revision.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,14 @@ func (d *revision) differentiateHelm3() error {
8787
}
8888
switch len(d.revisions) {
8989
case 1:
90-
releaseResponse, err := getRelease(d.release, namespace, d.kubeContext)
90+
releaseResponse, err := getRelease(d.release, 0, namespace, d.kubeContext)
9191

9292
if err != nil {
9393
return err
9494
}
9595

9696
revision, _ := strconv.Atoi(d.revisions[0])
97-
revisionResponse, err := getRevision(d.release, revision, namespace, d.kubeContext)
97+
revisionResponse, err := getRelease(d.release, revision, namespace, d.kubeContext)
9898
if err != nil {
9999
return err
100100
}
@@ -117,12 +117,12 @@ func (d *revision) differentiateHelm3() error {
117117
revision1, revision2 = revision2, revision1
118118
}
119119

120-
revisionResponse1, err := getRevision(d.release, revision1, namespace, d.kubeContext)
120+
revisionResponse1, err := getRelease(d.release, revision1, namespace, d.kubeContext)
121121
if err != nil {
122122
return err
123123
}
124124

125-
revisionResponse2, err := getRevision(d.release, revision2, namespace, d.kubeContext)
125+
revisionResponse2, err := getRelease(d.release, revision2, namespace, d.kubeContext)
126126
if err != nil {
127127
return err
128128
}

cmd/rollback.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,15 +76,15 @@ func (d *rollback) backcastHelm3() error {
7676
excludes = []string{}
7777
}
7878
// get manifest of the latest release
79-
releaseResponse, err := getRelease(d.release, namespace, d.kubeContext)
79+
releaseResponse, err := getRelease(d.release, 0, namespace, d.kubeContext)
8080

8181
if err != nil {
8282
return err
8383
}
8484

8585
// get manifest of the release to rollback
8686
revision, _ := strconv.Atoi(d.revisions[0])
87-
revisionResponse, err := getRevision(d.release, revision, namespace, d.kubeContext)
87+
revisionResponse, err := getRelease(d.release, revision, namespace, d.kubeContext)
8888
if err != nil {
8989
return err
9090
}

cmd/upgrade.go

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ type diffCmd struct {
7272
extraAPIs []string
7373
kubeVersion string
7474
useUpgradeDryRun bool
75+
revision int // 0 = newest, which is what helm returns by default.
7576
diff.Options
7677

7778
// dryRunMode can take the following values:
@@ -111,6 +112,21 @@ func (d *diffCmd) clusterAccessAllowed() bool {
111112
return d.dryRunMode == dryRunNone || d.dryRunMode == envFalse || d.dryRunMode == dryRunServer
112113
}
113114

115+
// validateRevision checks the --revision flag, which is only meaningful when the
116+
// flag was set and helm-diff is allowed to read the release from the cluster.
117+
func (d *diffCmd) validateRevision(changed bool) error {
118+
if !changed {
119+
return nil
120+
}
121+
if d.revision < 1 {
122+
return fmt.Errorf("flag %q must be a positive revision number, but got %d", "revision", d.revision)
123+
}
124+
if !d.clusterAccessAllowed() {
125+
return fmt.Errorf("flag %q requires cluster access, so it cannot be used with --dry-run=%s", "revision", d.dryRunMode)
126+
}
127+
return nil
128+
}
129+
114130
const globalUsage = `Show a diff explaining what a helm upgrade would change.
115131
116132
This fetches the currently deployed version of a release
@@ -173,6 +189,10 @@ func newChartCommand() *cobra.Command {
173189
return fmt.Errorf("flag %q must be %q, %q or %q, but got %q", "server-side", envTrue, envFalse, serverSideAuto, diff.serverSide)
174190
}
175191

192+
if err := diff.validateRevision(cmd.Flags().Changed("revision")); err != nil {
193+
return err
194+
}
195+
176196
// Suppress the command usage on error. See #77 for more info
177197
cmd.SilenceUsage = true
178198

@@ -259,6 +279,7 @@ func newChartCommand() *cobra.Command {
259279
f.BoolVar(&diff.insecureSkipTLSVerify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the chart download")
260280
f.BoolVar(&diff.normalizeManifests, "normalize-manifests", false, "normalize manifests before running diff to exclude style differences from the output")
261281
f.BoolVar(&diff.takeOwnership, "take-ownership", false, "if set, upgrade will ignore the check for helm annotations and take ownership of the existing resources")
282+
f.IntVar(&diff.revision, "revision", 0, "revision of the release to use as the diff baseline instead of the newest one")
262283
f.StringVar(&diff.serverSide, "server-side", serverSideAuto, `must be "true", "false" or "auto". Object updates run in the server instead of the client ("auto" defaults the value from the previous chart release's method)`)
263284

264285
AddDiffOptions(f, &diff.Options)
@@ -282,11 +303,14 @@ func (d *diffCmd) runHelm3() error {
282303
}
283304

284305
if d.clusterAccessAllowed() {
285-
releaseManifest, err = getRelease(d.release, d.namespace, d.kubeContext)
306+
releaseManifest, err = getRelease(d.release, d.revision, d.namespace, d.kubeContext)
286307
}
287308

288309
var newInstall bool
289310
if err != nil && strings.Contains(err.Error(), "release: not found") {
311+
if d.revision > 0 {
312+
return fmt.Errorf("Failed to get revision %d of release %s in namespace %s: %w", d.revision, d.release, d.namespace, err)
313+
}
290314
if d.isAllowUnreleased() {
291315
newInstall = true
292316
err = nil
@@ -326,7 +350,7 @@ func (d *diffCmd) runHelm3() error {
326350
currentSpecs := make(map[string]*manifest.MappingResult)
327351
if !newInstall && d.clusterAccessAllowed() {
328352
if !d.noHooks && !d.threeWayMerge {
329-
hooks, err := getHooks(d.release, d.namespace, d.kubeContext)
353+
hooks, err := getHooks(d.release, d.revision, d.namespace, d.kubeContext)
330354
if err != nil {
331355
return err
332356
}

cmd/upgrade_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,3 +205,31 @@ func TestServerSideFlagValidation(t *testing.T) {
205205
})
206206
}
207207
}
208+
209+
func TestValidateRevision(t *testing.T) {
210+
cases := []struct {
211+
name string
212+
revision int
213+
changed bool
214+
dryRunMode string
215+
expectErr bool
216+
}{
217+
{name: "unset", revision: 0, changed: false, dryRunMode: dryRunNone, expectErr: false},
218+
{name: "positive revision", revision: 2, changed: true, dryRunMode: dryRunNone, expectErr: false},
219+
{name: "positive revision with dry-run=server", revision: 2, changed: true, dryRunMode: dryRunServer, expectErr: false},
220+
{name: "explicit zero", revision: 0, changed: true, dryRunMode: dryRunNone, expectErr: true},
221+
{name: "negative revision", revision: -1, changed: true, dryRunMode: dryRunNone, expectErr: true},
222+
{name: "dry-run=client denies cluster access", revision: 2, changed: true, dryRunMode: dryRunNoOptDefVal, expectErr: true},
223+
{name: "dry-run=true denies cluster access", revision: 2, changed: true, dryRunMode: envTrue, expectErr: true},
224+
}
225+
226+
for _, tc := range cases {
227+
t.Run(tc.name, func(t *testing.T) {
228+
d := diffCmd{revision: tc.revision, dryRunMode: tc.dryRunMode}
229+
err := d.validateRevision(tc.changed)
230+
if (err != nil) != tc.expectErr {
231+
t.Errorf("expected error=%v, got %v", tc.expectErr, err)
232+
}
233+
})
234+
}
235+
}

0 commit comments

Comments
 (0)