Skip to content

Commit 25c4738

Browse files
committedMar 13, 2024·
Enabling hide secrets on install and upgrade dry run
This change adds a new flag to the install and upgrade commands in the Helm client and properties to the install and upgrade action. The new flag is --hide-secret and can only be used with the --dry-run flag. The --dry-run flag is designed to send all chart rendered manifests to stdout so that they can be inspected. When the --hide-secret flag is used the Secret content is removed from the output. Signed-off-by: Matt Farina <matt.farina@suse.com>
1 parent fa47752 commit 25c4738

16 files changed

+316
-13
lines changed
 

‎cmd/helm/install.go

+6-2
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,8 @@ To check the generated manifests of a release without installing the chart,
9797
the --debug and --dry-run flags can be combined.
9898
9999
The --dry-run flag will output all generated chart manifests, including Secrets
100-
which can contain sensitive values. Please carefully consider how and when this
101-
flag is used.
100+
which can contain sensitive values. To hide Kubernetes Secrets use the
101+
--hide-secret flag. Please carefully consider how and when these flags are used.
102102
103103
If --verify is set, the chart MUST have a provenance file, and the provenance
104104
file MUST pass all verification steps.
@@ -163,6 +163,10 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
163163
}
164164

165165
addInstallFlags(cmd, cmd.Flags(), client, valueOpts)
166+
// hide-secret is not available in all places the install flags are used so
167+
// it is added separately
168+
f := cmd.Flags()
169+
f.BoolVar(&client.HideSecret, "hide-secret", false, "hide Kubernetes Secrets when also using the --dry-run flag")
166170
bindOutputFlag(cmd, &outfmt)
167171
bindPostRenderFlag(cmd, &client.PostRenderer)
168172

‎cmd/helm/install_test.go

+16
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,22 @@ func TestInstall(t *testing.T) {
252252
cmd: fmt.Sprintf("install aeneas test/reqtest --username username --password password --repository-config %s --repository-cache %s", repoFile, srv.Root()),
253253
golden: "output/install.txt",
254254
},
255+
{
256+
name: "dry-run displaying secret",
257+
cmd: "install secrets testdata/testcharts/chart-with-secret --dry-run",
258+
golden: "output/install-dry-run-with-secret.txt",
259+
},
260+
{
261+
name: "dry-run hiding secret",
262+
cmd: "install secrets testdata/testcharts/chart-with-secret --dry-run --hide-secret",
263+
golden: "output/install-dry-run-with-secret-hidden.txt",
264+
},
265+
{
266+
name: "hide-secret error without dry-run",
267+
cmd: "install secrets testdata/testcharts/chart-with-secret --hide-secret",
268+
wantError: true,
269+
golden: "output/install-hide-secret.txt",
270+
},
255271
}
256272

257273
runTestCmd(t, tests)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
NAME: secrets
2+
LAST DEPLOYED: Fri Sep 2 22:04:05 1977
3+
NAMESPACE: default
4+
STATUS: pending-install
5+
REVISION: 1
6+
TEST SUITE: None
7+
HOOKS:
8+
MANIFEST:
9+
---
10+
# Source: chart-with-secret/templates/secret.yaml
11+
# HIDDEN: The Secret output has been suppressed
12+
---
13+
# Source: chart-with-secret/templates/configmap.yaml
14+
apiVersion: v1
15+
kind: ConfigMap
16+
metadata:
17+
name: test-configmap
18+
data:
19+
foo: bar
20+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
NAME: secrets
2+
LAST DEPLOYED: Fri Sep 2 22:04:05 1977
3+
NAMESPACE: default
4+
STATUS: pending-install
5+
REVISION: 1
6+
TEST SUITE: None
7+
HOOKS:
8+
MANIFEST:
9+
---
10+
# Source: chart-with-secret/templates/secret.yaml
11+
apiVersion: v1
12+
kind: Secret
13+
metadata:
14+
name: test-secret
15+
stringData:
16+
foo: bar
17+
---
18+
# Source: chart-with-secret/templates/configmap.yaml
19+
apiVersion: v1
20+
kind: ConfigMap
21+
metadata:
22+
name: test-configmap
23+
data:
24+
foo: bar
25+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Error: INSTALLATION FAILED: Hiding Kubernetes secrets requires a dry-run mode
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
apiVersion: v2
2+
description: Chart with Kubernetes Secret
3+
name: chart-with-secret
4+
version: 0.0.1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
apiVersion: v1
2+
kind: ConfigMap
3+
metadata:
4+
name: test-configmap
5+
data:
6+
foo: bar
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
apiVersion: v1
2+
kind: Secret
3+
metadata:
4+
name: test-secret
5+
stringData:
6+
foo: bar

‎cmd/helm/upgrade.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ or '--set' flags. Priority is given to new values.
7474
$ helm upgrade --reuse-values --set foo=bar --set foo=newbar redis ./redis
7575
7676
The --dry-run flag will output all generated chart manifests, including Secrets
77-
which can contain sensitive values. Please carefully consider how and when this
78-
flag is used.
77+
which can contain sensitive values. To hide Kubernetes Secrets use the
78+
--hide-secret flag. Please carefully consider how and when these flags are used.
7979
`
8080

8181
func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
@@ -146,6 +146,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
146146
instClient.DependencyUpdate = client.DependencyUpdate
147147
instClient.Labels = client.Labels
148148
instClient.EnableDNS = client.EnableDNS
149+
instClient.HideSecret = client.HideSecret
149150

150151
rel, err := runInstall(args, instClient, valueOpts, out)
151152
if err != nil {
@@ -246,6 +247,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
246247
f.BoolVarP(&client.Install, "install", "i", false, "if a release by this name doesn't already exist, run an install")
247248
f.BoolVar(&client.Devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored")
248249
f.StringVar(&client.DryRunOption, "dry-run", "", "simulate an install. If --dry-run is set with no option being specified or as '--dry-run=client', it will not attempt cluster connections. Setting '--dry-run=server' allows attempting cluster connections.")
250+
f.BoolVar(&client.HideSecret, "hide-secret", false, "hide Kubernetes Secrets when also using the --dry-run flag")
249251
f.Lookup("dry-run").NoOptDefVal = "client"
250252
f.BoolVar(&client.Recreate, "recreate-pods", false, "performs pods restart for the resource if applicable")
251253
f.MarkDeprecated("recreate-pods", "functionality will no longer be updated. Consult the documentation for other methods to recreate pods")

‎cmd/helm/upgrade_test.go

+101
Original file line numberDiff line numberDiff line change
@@ -458,3 +458,104 @@ func TestUpgradeInstallWithLabels(t *testing.T) {
458458
t.Errorf("Expected {%v}, got {%v}", expectedLabels, updatedRel.Labels)
459459
}
460460
}
461+
462+
func prepareMockReleaseWithSecret(releaseName string, t *testing.T) (func(n string, v int, ch *chart.Chart) *release.Release, *chart.Chart, string) {
463+
tmpChart := t.TempDir()
464+
configmapData, err := os.ReadFile("testdata/testcharts/chart-with-secret/templates/configmap.yaml")
465+
if err != nil {
466+
t.Fatalf("Error loading template yaml %v", err)
467+
}
468+
secretData, err := os.ReadFile("testdata/testcharts/chart-with-secret/templates/secret.yaml")
469+
if err != nil {
470+
t.Fatalf("Error loading template yaml %v", err)
471+
}
472+
cfile := &chart.Chart{
473+
Metadata: &chart.Metadata{
474+
APIVersion: chart.APIVersionV1,
475+
Name: "testUpgradeChart",
476+
Description: "A Helm chart for Kubernetes",
477+
Version: "0.1.0",
478+
},
479+
Templates: []*chart.File{{Name: "templates/configmap.yaml", Data: configmapData}, {Name: "templates/secret.yaml", Data: secretData}},
480+
}
481+
chartPath := filepath.Join(tmpChart, cfile.Metadata.Name)
482+
if err := chartutil.SaveDir(cfile, tmpChart); err != nil {
483+
t.Fatalf("Error creating chart for upgrade: %v", err)
484+
}
485+
ch, err := loader.Load(chartPath)
486+
if err != nil {
487+
t.Fatalf("Error loading chart: %v", err)
488+
}
489+
_ = release.Mock(&release.MockReleaseOptions{
490+
Name: releaseName,
491+
Chart: ch,
492+
})
493+
494+
relMock := func(n string, v int, ch *chart.Chart) *release.Release {
495+
return release.Mock(&release.MockReleaseOptions{Name: n, Version: v, Chart: ch})
496+
}
497+
498+
return relMock, ch, chartPath
499+
}
500+
501+
func TestUpgradeWithDryRun(t *testing.T) {
502+
releaseName := "funny-bunny-labels"
503+
_, _, chartPath := prepareMockReleaseWithSecret(releaseName, t)
504+
505+
defer resetEnv()()
506+
507+
store := storageFixture()
508+
509+
// First install a release into the store so that future --dry-run attempts
510+
// have it available.
511+
cmd := fmt.Sprintf("upgrade %s --install '%s'", releaseName, chartPath)
512+
_, _, err := executeActionCommandC(store, cmd)
513+
if err != nil {
514+
t.Errorf("unexpected error, got '%v'", err)
515+
}
516+
517+
_, err = store.Get(releaseName, 1)
518+
if err != nil {
519+
t.Errorf("unexpected error, got '%v'", err)
520+
}
521+
522+
cmd = fmt.Sprintf("upgrade %s --dry-run '%s'", releaseName, chartPath)
523+
_, out, err := executeActionCommandC(store, cmd)
524+
if err != nil {
525+
t.Errorf("unexpected error, got '%v'", err)
526+
}
527+
528+
// No second release should be stored because this is a dry run.
529+
_, err = store.Get(releaseName, 2)
530+
if err == nil {
531+
t.Error("expected error as there should be no new release but got none")
532+
}
533+
534+
if !strings.Contains(out, "kind: Secret") {
535+
t.Error("expected secret in output from --dry-run but found none")
536+
}
537+
538+
// Ensure the secret is not in the output
539+
cmd = fmt.Sprintf("upgrade %s --dry-run --hide-secret '%s'", releaseName, chartPath)
540+
_, out, err = executeActionCommandC(store, cmd)
541+
if err != nil {
542+
t.Errorf("unexpected error, got '%v'", err)
543+
}
544+
545+
// No second release should be stored because this is a dry run.
546+
_, err = store.Get(releaseName, 2)
547+
if err == nil {
548+
t.Error("expected error as there should be no new release but got none")
549+
}
550+
551+
if strings.Contains(out, "kind: Secret") {
552+
t.Error("expected no secret in output from --dry-run --hide-secret but found one")
553+
}
554+
555+
// Ensure there is an error when --hide-secret used without dry-run
556+
cmd = fmt.Sprintf("upgrade %s --hide-secret '%s'", releaseName, chartPath)
557+
_, _, err = executeActionCommandC(store, cmd)
558+
if err == nil {
559+
t.Error("expected error when --hide-secret used without --dry-run")
560+
}
561+
}

‎pkg/action/action.go

+6-2
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ type Configuration struct {
103103
// TODO: As part of the refactor the duplicate code in cmd/helm/template.go should be removed
104104
//
105105
// This code has to do with writing files to disk.
106-
func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrender.PostRenderer, interactWithRemote, enableDNS bool) ([]*release.Hook, *bytes.Buffer, string, error) {
106+
func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrender.PostRenderer, interactWithRemote, enableDNS, hideSecret bool) ([]*release.Hook, *bytes.Buffer, string, error) {
107107
hs := []*release.Hook{}
108108
b := bytes.NewBuffer(nil)
109109

@@ -200,7 +200,11 @@ func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Valu
200200

201201
for _, m := range manifests {
202202
if outputDir == "" {
203-
fmt.Fprintf(b, "---\n# Source: %s\n%s\n", m.Name, m.Content)
203+
if hideSecret && m.Head.Kind == "Secret" && m.Head.Version == "v1" {
204+
fmt.Fprintf(b, "---\n# Source: %s\n# HIDDEN: The Secret output has been suppressed\n", m.Name)
205+
} else {
206+
fmt.Fprintf(b, "---\n# Source: %s\n%s\n", m.Name, m.Content)
207+
}
204208
} else {
205209
newDir := outputDir
206210
if useReleaseName {

‎pkg/action/action_test.go

+7
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,13 @@ func withSampleTemplates() chartOption {
195195
}
196196
}
197197

198+
func withSampleSecret() chartOption {
199+
return func(opts *chartOptions) {
200+
sampleSecret := &chart.File{Name: "templates/secret.yaml", Data: []byte("apiVersion: v1\nkind: Secret\n")}
201+
opts.Templates = append(opts.Templates, sampleSecret)
202+
}
203+
}
204+
198205
func withSampleIncludingIncorrectTemplates() chartOption {
199206
return func(opts *chartOptions) {
200207
sampleTemplates := []*chart.File{

‎pkg/action/install.go

+14-6
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,14 @@ type Install struct {
6969

7070
ChartPathOptions
7171

72-
ClientOnly bool
73-
Force bool
74-
CreateNamespace bool
75-
DryRun bool
76-
DryRunOption string
72+
ClientOnly bool
73+
Force bool
74+
CreateNamespace bool
75+
DryRun bool
76+
DryRunOption string
77+
// HideSecret can be set to true when DryRun is enabled in order to hide
78+
// Kubernetes Secrets in the output. It cannot be used outside of DryRun.
79+
HideSecret bool
7780
DisableHooks bool
7881
Replace bool
7982
Wait bool
@@ -230,6 +233,11 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
230233
}
231234
}
232235

236+
// HideSecret must be used with dry run. Otherwise, return an error.
237+
if !i.isDryRun() && i.HideSecret {
238+
return nil, errors.New("Hiding Kubernetes secrets requires a dry-run mode")
239+
}
240+
233241
if err := i.availableName(); err != nil {
234242
return nil, err
235243
}
@@ -301,7 +309,7 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
301309
rel := i.createRelease(chrt, vals, i.Labels)
302310

303311
var manifestDoc *bytes.Buffer
304-
rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer, interactWithRemote, i.EnableDNS)
312+
rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer, interactWithRemote, i.EnableDNS, i.HideSecret)
305313
// Even for errors, attach this if available
306314
if manifestDoc != nil {
307315
rel.Manifest = manifestDoc.String()

‎pkg/action/install_test.go

+40
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,46 @@ func TestInstallRelease_DryRun(t *testing.T) {
255255
is.Equal(res.Info.Description, "Dry run complete")
256256
}
257257

258+
func TestInstallRelease_DryRunHiddenSecret(t *testing.T) {
259+
is := assert.New(t)
260+
instAction := installAction(t)
261+
262+
// First perform a normal dry-run with the secret and confirm its presence.
263+
instAction.DryRun = true
264+
vals := map[string]interface{}{}
265+
res, err := instAction.Run(buildChart(withSampleSecret(), withSampleTemplates()), vals)
266+
if err != nil {
267+
t.Fatalf("Failed install: %s", err)
268+
}
269+
is.Contains(res.Manifest, "---\n# Source: hello/templates/secret.yaml\napiVersion: v1\nkind: Secret")
270+
271+
_, err = instAction.cfg.Releases.Get(res.Name, res.Version)
272+
is.Error(err)
273+
is.Equal(res.Info.Description, "Dry run complete")
274+
275+
// Perform a dry-run where the secret should not be present
276+
instAction.HideSecret = true
277+
vals = map[string]interface{}{}
278+
res2, err := instAction.Run(buildChart(withSampleSecret(), withSampleTemplates()), vals)
279+
if err != nil {
280+
t.Fatalf("Failed install: %s", err)
281+
}
282+
283+
is.NotContains(res2.Manifest, "---\n# Source: hello/templates/secret.yaml\napiVersion: v1\nkind: Secret")
284+
285+
_, err = instAction.cfg.Releases.Get(res2.Name, res2.Version)
286+
is.Error(err)
287+
is.Equal(res2.Info.Description, "Dry run complete")
288+
289+
// Ensure there is an error when HideSecret True but not in a dry-run mode
290+
instAction.DryRun = false
291+
vals = map[string]interface{}{}
292+
_, err = instAction.Run(buildChart(withSampleSecret(), withSampleTemplates()), vals)
293+
if err == nil {
294+
t.Fatalf("Did not get expected an error when dry-run false and hide secret is true")
295+
}
296+
}
297+
258298
// Regression test for #7955
259299
func TestInstallRelease_DryRun_Lookup(t *testing.T) {
260300
is := assert.New(t)

‎pkg/action/upgrade.go

+9-1
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ type Upgrade struct {
7474
DryRun bool
7575
// DryRunOption controls whether the operation is prepared, but not executed with options on whether or not to interact with the remote cluster.
7676
DryRunOption string
77+
// HideSecret can be set to true when DryRun is enabled in order to hide
78+
// Kubernetes Secrets in the output. It cannot be used outside of DryRun.
79+
HideSecret bool
7780
// Force will, if set to `true`, ignore certain warnings and perform the upgrade anyway.
7881
//
7982
// This should be used with caution.
@@ -191,6 +194,11 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin
191194
return nil, nil, errMissingChart
192195
}
193196

197+
// HideSecret must be used with dry run. Otherwise, return an error.
198+
if !u.isDryRun() && u.HideSecret {
199+
return nil, nil, errors.New("Hiding Kubernetes secrets requires a dry-run mode")
200+
}
201+
194202
// finds the last non-deleted release with the given name
195203
lastRelease, err := u.cfg.Releases.Last(name)
196204
if err != nil {
@@ -259,7 +267,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin
259267
interactWithRemote = true
260268
}
261269

262-
hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer, interactWithRemote, u.EnableDNS)
270+
hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer, interactWithRemote, u.EnableDNS, u.HideSecret)
263271
if err != nil {
264272
return nil, nil, err
265273
}

‎pkg/action/upgrade_test.go

+51
Original file line numberDiff line numberDiff line change
@@ -535,3 +535,54 @@ func TestUpgradeRelease_SystemLabels(t *testing.T) {
535535

536536
is.Equal(fmt.Errorf("user suplied labels contains system reserved label name. System labels: %+v", driver.GetSystemLabels()), err)
537537
}
538+
539+
func TestUpgradeRelease_DryRun(t *testing.T) {
540+
is := assert.New(t)
541+
req := require.New(t)
542+
543+
upAction := upgradeAction(t)
544+
rel := releaseStub()
545+
rel.Name = "previous-release"
546+
rel.Info.Status = release.StatusDeployed
547+
req.NoError(upAction.cfg.Releases.Create(rel))
548+
549+
upAction.DryRun = true
550+
vals := map[string]interface{}{}
551+
552+
ctx, done := context.WithCancel(context.Background())
553+
res, err := upAction.RunWithContext(ctx, rel.Name, buildChart(withSampleSecret()), vals)
554+
done()
555+
req.NoError(err)
556+
is.Equal(release.StatusPendingUpgrade, res.Info.Status)
557+
is.Contains(res.Manifest, "kind: Secret")
558+
559+
lastRelease, err := upAction.cfg.Releases.Last(rel.Name)
560+
req.NoError(err)
561+
is.Equal(lastRelease.Info.Status, release.StatusDeployed)
562+
is.Equal(1, lastRelease.Version)
563+
564+
// Test the case for hiding the secret to ensure it is not displayed
565+
upAction.HideSecret = true
566+
vals = map[string]interface{}{}
567+
568+
ctx, done = context.WithCancel(context.Background())
569+
res, err = upAction.RunWithContext(ctx, rel.Name, buildChart(withSampleSecret()), vals)
570+
done()
571+
req.NoError(err)
572+
is.Equal(release.StatusPendingUpgrade, res.Info.Status)
573+
is.NotContains(res.Manifest, "kind: Secret")
574+
575+
lastRelease, err = upAction.cfg.Releases.Last(rel.Name)
576+
req.NoError(err)
577+
is.Equal(lastRelease.Info.Status, release.StatusDeployed)
578+
is.Equal(1, lastRelease.Version)
579+
580+
// Ensure in a dry run mode when using HideSecret
581+
upAction.DryRun = false
582+
vals = map[string]interface{}{}
583+
584+
ctx, done = context.WithCancel(context.Background())
585+
_, err = upAction.RunWithContext(ctx, rel.Name, buildChart(withSampleSecret()), vals)
586+
done()
587+
req.Error(err)
588+
}

0 commit comments

Comments
 (0)
Please sign in to comment.