Repository: helm/chart-testing
Pull request #284 (2020-10-07 08:03:03)

Fix typo

**What this PR does / why we need it**:
fix typo

**Which issue this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close that issue when PR gets merged)*: 
`none`

**Special notes for your reviewer**:
`none`


Patch:
diff --git a/Dockerfile b/Dockerfile
index 6d2781b..c2b6c57 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,6 +1,7 @@
 FROM alpine:3.12
 
 RUN apk --no-cache add \
+    bash \
     curl \
     git \
     libc6-compat \
diff --git a/README.md b/README.md
index aa15405..e69c3c6 100644
--- a/README.md
+++ b/README.md
@@ -51,6 +51,9 @@ See documentation for individual commands:
 * [ct list-changed](doc/ct_list-changed.md)
 * [ct version](doc/ct_version.md)
 
+For a more extensive how-to guide, please see:
+
+* [charts-repo-actions-demo](https://github.com/helm/charts-repo-actions-demo)
 
 ## Configuration
 
@@ -198,7 +201,6 @@ Run the script specifying the version for the new release.
 
 Versions must start with a lower-case `v`, e. g. `v3.1.1`.
 
-
 ## Supported versions
 
 The previous MAJOR version will be supported for three months after each new MAJOR release.
diff --git a/ct/cmd/lint.go b/ct/cmd/lint.go
index cc32142..6580db9 100644
--- a/ct/cmd/lint.go
+++ b/ct/cmd/lint.go
@@ -69,6 +69,11 @@ func addLintFlags(flags *flag.FlagSet) {
 			Enable schema validation of 'Chart.yaml' using Yamale (default: true)`))
 	flags.Bool("validate-yaml", true, heredoc.Doc(`
 			Enable linting of 'Chart.yaml' and values files (default: true)`))
+	flags.StringSlice("additional-commands", []string{}, heredoc.Doc(`
+            Additional commands to run per chart (default: [])
+            Commands will be executed in the same order as provided in the list and will
+            be rendered with go template before being executed.
+            Example: "helm unittest --helm3 -f tests/*.yaml {{ .Path }}"`))
 }
 
 func lint(cmd *cobra.Command, args []string) error {
diff --git a/go.mod b/go.mod
index a311720..a35e549 100644
--- a/go.mod
+++ b/go.mod
@@ -9,6 +9,7 @@ require (
 	github.com/goreleaser/goreleaser v0.129.0
 	github.com/hashicorp/go-multierror v1.0.0
 	github.com/hashicorp/go-retryablehttp v0.6.4
+	github.com/mattn/go-shellwords v1.0.10
 	github.com/mitchellh/go-homedir v1.1.0
 	github.com/mitchellh/mapstructure v1.2.2 // indirect
 	github.com/pelletier/go-toml v1.6.0 // indirect
diff --git a/go.sum b/go.sum
index d118684..64c1511 100644
--- a/go.sum
+++ b/go.sum
@@ -219,6 +219,8 @@ github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
 github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
 github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
 github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
+github.com/mattn/go-shellwords v1.0.10 h1:Y7Xqm8piKOO3v10Thp7Z36h4FYFjt5xB//6XvOrs2Gw=
+github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
 github.com/mattn/go-zglob v0.0.1 h1:xsEx/XUoVlI6yXjqBK062zYhRTZltCNmYPx6v+8DNaY=
 github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
diff --git a/pkg/chart/chart.go b/pkg/chart/chart.go
index 07c04f1..3ed7b14 100644
--- a/pkg/chart/chart.go
+++ b/pkg/chart/chart.go
@@ -131,6 +131,13 @@ type Linter interface {
 	Yamale(yamlFile string, schemaFile string) error
 }
 
+// CmdExecutor is the interface
+//
+// RunCommand renders cmdTemplate as go template using data and executes the resulting command
+type CmdExecutor interface {
+	RunCommand(cmdTemplate string, data interface{}) error
+}
+
 // DirectoryLister is the interface
 //
 // ListChildDirs lists direct child directories of parentDir given they pass the test function
@@ -152,7 +159,7 @@ type AccountValidator interface {
 	Validate(repoDomain string, account string) error
 }
 
-// Chart represents a Helm chart, and can be initalized with the NewChart method.
+// Chart represents a Helm chart, and can be initialized with the NewChart method.
 type Chart struct {
 	path          string
 	yaml          *util.ChartYaml
@@ -224,6 +231,7 @@ type Testing struct {
 	kubectl                  Kubectl
 	git                      Git
 	linter                   Linter
+	cmdExecutor              CmdExecutor
 	accountValidator         AccountValidator
 	directoryLister          DirectoryLister
 	chartUtils               ChartUtils
@@ -253,6 +261,7 @@ func NewTesting(config config.Configuration) (Testing, error) {
 		git:              tool.NewGit(procExec),
 		kubectl:          tool.NewKubectl(procExec),
 		linter:           tool.NewLinter(procExec),
+		cmdExecutor:      tool.NewCmdTemplateExecutor(procExec),
 		accountValidator: tool.AccountValidator{},
 		directoryLister:  util.DirectoryLister{},
 		chartUtils:       util.ChartUtils{},
@@ -451,6 +460,13 @@ func (t *Testing) LintChart(chart *Chart) TestResult {
 		}
 	}
 
+	for _, cmd := range t.config.AdditionalCommands {
+		if err := t.cmdExecutor.RunCommand(cmd, chart); err != nil {
+			result.Error = err
+			return result
+		}
+	}
+
 	// Lint with defaults if no values files are specified.
 	if len(valuesFiles) == 0 {
 		valuesFiles = append(valuesFiles, "")
diff --git a/pkg/config/config.go b/pkg/config/config.go
index ec62abe..a88d402 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -50,6 +50,7 @@ type Configuration struct {
 	ValidateMaintainers   bool     `mapstructure:"validate-maintainers"`
 	ValidateChartSchema   bool     `mapstructure:"validate-chart-schema"`
 	ValidateYaml          bool     `mapstructure:"validate-yaml"`
+	AdditionalCommands    []string `mapstructure:"additional-commands"`
 	CheckVersionIncrement bool     `mapstructure:"check-version-increment"`
 	ProcessAllCharts      bool     `mapstructure:"all"`
 	Charts                []string `mapstructure:"charts"`
diff --git a/pkg/tool/cmdexecutor.go b/pkg/tool/cmdexecutor.go
new file mode 100644
index 0000000..0449088
--- /dev/null
+++ b/pkg/tool/cmdexecutor.go
@@ -0,0 +1,38 @@
+package tool
+
+import (
+	"strings"
+	"text/template"
+
+	"github.com/mattn/go-shellwords"
+)
+
+type ProcessExecutor interface {
+	RunProcess(executable string, execArgs ...interface{}) error
+}
+
+type CmdTemplateExecutor struct {
+	exec ProcessExecutor
+}
+
+func NewCmdTemplateExecutor(exec ProcessExecutor) CmdTemplateExecutor {
+	return CmdTemplateExecutor{
+		exec: exec,
+	}
+}
+
+func (t CmdTemplateExecutor) RunCommand(cmdTemplate string, data interface{}) error {
+	var template = template.Must(template.New("command").Parse(cmdTemplate))
+	var b strings.Builder
+	if err := template.Execute(&b, data); err != nil {
+		return err
+	}
+	rendered := b.String()
+
+	words, err := shellwords.Parse(rendered)
+	if err != nil {
+		return err
+	}
+	name, args := words[0], words[1:]
+	return t.exec.RunProcess(name, args)
+}


Test patch:
diff --git a/pkg/chart/chart_test.go b/pkg/chart/chart_test.go
index 13a1196..829ccf8 100644
--- a/pkg/chart/chart_test.go
+++ b/pkg/chart/chart_test.go
@@ -109,6 +109,15 @@ func (h fakeHelm) Version() (string, error) {
 	return "v3.0.0", nil
 }
 
+type fakeCmdExecutor struct {
+	mock.Mock
+}
+
+func (c *fakeCmdExecutor) RunCommand(cmdTemplate string, data interface{}) error {
+	c.Called(cmdTemplate, data)
+	return nil
+}
+
 var ct Testing
 
 func init() {
@@ -417,3 +426,47 @@ func TestChart_HasCIValuesFile(t *testing.T) {
 		})
 	}
 }
+
+func TestChart_AdditionalCommandsAreRun(t *testing.T) {
+	type testData struct {
+		name            string
+		cfg             config.Configuration
+		callsRunCommand int
+	}
+
+	testCases := []testData{
+		{
+			name:            "no additional commands",
+			cfg:             config.Configuration{},
+			callsRunCommand: 0,
+		},
+		{
+			name: "one command",
+			cfg: config.Configuration{
+				AdditionalCommands: []string{"helm unittest --helm3 -f tests/*.yaml {{ .Path }}"},
+			},
+			callsRunCommand: 1,
+		},
+		{
+			name: "multiple commands",
+			cfg: config.Configuration{
+				AdditionalCommands: []string{"echo", "helm unittest --helm3 -f tests/*.yaml {{ .Path }}"},
+			},
+			callsRunCommand: 2,
+		},
+	}
+
+	for _, testData := range testCases {
+		t.Run(testData.name, func(t *testing.T) {
+			fakeCmdExecutor := new(fakeCmdExecutor)
+			fakeCmdExecutor.On("RunCommand", mock.Anything, mock.Anything).Return(nil)
+
+			ct := newTestingMock(testData.cfg)
+			ct.cmdExecutor = fakeCmdExecutor
+
+			ct.LintChart(&Chart{})
+
+			fakeCmdExecutor.AssertNumberOfCalls(t, "RunCommand", testData.callsRunCommand)
+		})
+	}
+}
diff --git a/pkg/tool/cmdexecutor_test.go b/pkg/tool/cmdexecutor_test.go
new file mode 100644
index 0000000..21cb6ea
--- /dev/null
+++ b/pkg/tool/cmdexecutor_test.go
@@ -0,0 +1,76 @@
+package tool
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/mock"
+)
+
+type fakeProcessExecutor struct {
+	mock.Mock
+}
+
+func (c *fakeProcessExecutor) RunProcess(executable string, execArgs ...interface{}) error {
+	c.Called(executable, execArgs[0])
+	return nil
+}
+
+func TestCmdTemplateExecutor_RunCommand(t *testing.T) {
+	type args struct {
+		cmdTemplate string
+		data        interface{}
+	}
+	tests := []struct {
+		name     string
+		args     args
+		wantErr  bool
+		validate func(t *testing.T, executor *fakeProcessExecutor)
+	}{
+		{
+			name: "command without arguments",
+			args: args{
+				cmdTemplate: "echo",
+				data:        nil,
+			},
+			validate: func(t *testing.T, executor *fakeProcessExecutor) {
+				executor.AssertCalled(t, "RunProcess", "echo", []string{})
+			},
+			wantErr: false,
+		},
+		{
+			name: "command with args",
+			args: args{
+				cmdTemplate: "echo hello world",
+			},
+			validate: func(t *testing.T, executor *fakeProcessExecutor) {
+				executor.AssertCalled(t, "RunProcess", "echo", []string{"hello", "world"})
+			},
+			wantErr: false,
+		},
+		{
+			name: "interpolate args",
+			args: args{
+				cmdTemplate: "helm unittest --helm3 -f tests/*.yaml {{ .Path }}",
+				data:        map[string]string{"Path": "charts/my-chart"},
+			},
+			validate: func(t *testing.T, executor *fakeProcessExecutor) {
+				executor.AssertCalled(t, "RunProcess", "helm", []string{"unittest", "--helm3", "-f", "tests/*.yaml", "charts/my-chart"})
+			},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			processExecutor := new(fakeProcessExecutor)
+			processExecutor.On("RunProcess", mock.Anything, mock.Anything).Return(nil)
+			templateExecutor := CmdTemplateExecutor{
+				exec: processExecutor,
+			}
+			if err := templateExecutor.RunCommand(tt.args.cmdTemplate, tt.args.data); (err != nil) != tt.wantErr {
+				t.Errorf("RunCommand() error = %v, wantErr %v", err, tt.wantErr)
+			}
+			tt.validate(t, processExecutor)
+
+		})
+	}
+}