Skip to content

Commit

Permalink
Support execution of additional commands during ct lint (#283)
Browse files Browse the repository at this point in the history
* support execution of additional commands for lint

Given that helm unittest is installed locally or mounted into the chart
testing container one could use this to run helm unittest for each
chart.

```
additional-commands:
 - helm unittest --helm3 -f tests/*.yaml {{ .Path }}
```

The command is treated as a go template. Like this it's possible to get
the path to the chart as in the example. It would also be open for
further extension if needed.

Signed-off-by: Torsten Walter <mail@torstenwalter.de>

* Use sh to execute command

Signed-off-by: Torsten Walter <mail@torstenwalter.de>

Co-authored-by: Reinhard Nägele <unguiculus@gmail.com>

* rename function to NewCmdTemplateExecutor

Signed-off-by: Torsten Walter <mail@torstenwalter.de>

* add error handling

Signed-off-by: Torsten Walter <mail@torstenwalter.de>

* add documentation

Signed-off-by: Torsten Walter <mail@torstenwalter.de>

* use go-shellwords to split rendered command

Signed-off-by: Torsten Walter <mail@torstenwalter.de>

* Update pkg/tool/cmdexecutor.go

Signed-off-by: Torsten Walter <mail@torstenwalter.de>

Co-authored-by: Reinhard Nägele <unguiculus@gmail.com>

* add unit tests

Signed-off-by: Torsten Walter <mail@torstenwalter.de>

Co-authored-by: Reinhard Nägele <unguiculus@gmail.com>
  • Loading branch information
torstenwalter and unguiculus authored Oct 20, 2020
1 parent f5e0860 commit e4d7b78
Show file tree
Hide file tree
Showing 9 changed files with 193 additions and 0 deletions.
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
FROM alpine:3.12

RUN apk --no-cache add \
bash \
curl \
git \
libc6-compat \
Expand Down
5 changes: 5 additions & 0 deletions ct/cmd/lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
16 changes: 16 additions & 0 deletions pkg/chart/chart.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -224,6 +231,7 @@ type Testing struct {
kubectl Kubectl
git Git
linter Linter
cmdExecutor CmdExecutor
accountValidator AccountValidator
directoryLister DirectoryLister
chartUtils ChartUtils
Expand Down Expand Up @@ -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{},
Expand Down Expand Up @@ -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, "")
Expand Down
53 changes: 53 additions & 0 deletions pkg/chart/chart_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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)
})
}
}
1 change: 1 addition & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
38 changes: 38 additions & 0 deletions pkg/tool/cmdexecutor.go
Original file line number Diff line number Diff line change
@@ -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)
}
76 changes: 76 additions & 0 deletions pkg/tool/cmdexecutor_test.go
Original file line number Diff line number Diff line change
@@ -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)

})
}
}

0 comments on commit e4d7b78

Please sign in to comment.