Add helmfile lint support (#162)

The use case is to have a list of helmfile releases version controlled together with all settings and have a CI pipeline that will lint all releases with settings before running sync. The new functionality was mostly copy pasted from the Diff implementation with some extra handling for fetching remote charts.

Notes:

* Added release name to chart path to avoid potential race condition when fetching the chart
This commit is contained in:
Johan Lyheden 2018-06-14 15:35:09 +02:00 committed by KUOKA Yusuke
parent 2fba241122
commit 6856c6e979
7 changed files with 198 additions and 0 deletions

View File

@ -155,6 +155,7 @@ COMMANDS:
repos sync repositories from state file (helm repo add && helm repo update)
charts sync charts from state file (helm upgrade --install)
diff diff charts from state file against env (helm diff)
lint lint charts from state file (helm lint)
sync sync all resources from state file (repos, charts and local chart deps)
status retrieve status of releases in state file
delete delete charts from state file (helm delete)
@ -211,6 +212,10 @@ The `helmfile test` sub-command runs a `helm test` against specified releases in
Use `--cleanup` to delete pods upon completion.
### lint
The `helmfile lint` sub-command runs a `helm lint` across all of the charts/releases defined in the manifest. Non local charts will be fetched into a temporary folder which will be deleted once the task is completed.
## Paths Overview
Using manifest files in conjunction with command line argument can be a bit confusing.

View File

@ -82,6 +82,18 @@ func (helm *execer) DiffRelease(name, chart string, flags ...string) error {
return err
}
func (helm *execer) Lint(chart string, flags ...string) error {
out, err := helm.exec(append([]string{"lint", chart}, flags...)...)
helm.write(out)
return err
}
func (helm *execer) Fetch(chart string, flags ...string) error {
out, err := helm.exec(append([]string{"fetch", chart}, flags...)...)
helm.write(out)
return err
}
func (helm *execer) DeleteRelease(name string, flags ...string) error {
out, err := helm.exec(append([]string{"delete", name}, flags...)...)
helm.write(out)

View File

@ -238,3 +238,23 @@ func Test_exec(t *testing.T) {
t.Errorf("helmexec.exec()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
func Test_Lint(t *testing.T) {
var buffer bytes.Buffer
helm := MockExecer(&buffer, "dev")
helm.Lint("path/to/chart", "--values", "file.yml")
expected := "exec: helm lint path/to/chart --values file.yml --kube-context dev\n"
if buffer.String() != expected {
t.Errorf("helmexec.Lint()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}
func Test_Fetch(t *testing.T) {
var buffer bytes.Buffer
helm := MockExecer(&buffer, "dev")
helm.Fetch("chart", "--version", "1.2.3", "--untar", "--untardir", "/tmp/dir")
expected := "exec: helm fetch chart --version 1.2.3 --untar --untardir /tmp/dir --kube-context dev\n"
if buffer.String() != expected {
t.Errorf("helmexec.Lint()\nactual = %v\nexpect = %v", buffer.String(), expected)
}
}

View File

@ -9,6 +9,8 @@ type Interface interface {
UpdateDeps(chart string) error
SyncRelease(name, chart string, flags ...string) error
DiffRelease(name, chart string, flags ...string) error
Fetch(chart string, flags ...string) error
Lint(chart string, flags ...string) error
ReleaseStatus(name string) error
DeleteRelease(name string, flags ...string) error
TestRelease(name string, flags ...string) error

33
main.go
View File

@ -155,6 +155,39 @@ func main() {
})
},
},
{
Name: "lint",
Usage: "lint charts from state file (helm lint)",
Flags: []cli.Flag{
cli.StringFlag{
Name: "args",
Value: "",
Usage: "pass args to helm exec",
},
cli.StringSliceFlag{
Name: "values",
Usage: "additional value files to be merged into the command",
},
cli.IntFlag{
Name: "concurrency",
Value: 0,
Usage: "maximum number of concurrent helm processes to run, 0 is unlimited",
},
},
Action: func(c *cli.Context) error {
return eachDesiredStateDo(c, func(state *state.HelmState, helm helmexec.Interface) []error {
args := c.String("args")
if len(args) > 0 {
helm.SetExtraArgs(strings.Split(args, " ")...)
}
values := c.StringSlice("values")
workers := c.Int("concurrency")
return state.LintReleases(helm, values, workers)
})
},
},
{
Name: "sync",
Usage: "sync all resources from state file (repos, charts and local chart deps)",

View File

@ -5,6 +5,7 @@ import (
"fmt"
"io/ioutil"
"os"
"path"
"path/filepath"
"strconv"
"strings"
@ -315,6 +316,120 @@ func (state *HelmState) DiffReleases(helm helmexec.Interface, additionalValues [
return nil
}
// LintReleases wrapper for executing helm lint on the releases
func (state *HelmState) LintReleases(helm helmexec.Interface, additionalValues []string, workerLimit int) []error {
var wgRelease sync.WaitGroup
var wgError sync.WaitGroup
errs := []error{}
jobQueue := make(chan *ReleaseSpec, len(state.Releases))
errQueue := make(chan error)
if workerLimit < 1 {
workerLimit = len(state.Releases)
}
wgRelease.Add(len(state.Releases))
// Create tmp directory and bail immediately if it fails
dir, err := ioutil.TempDir("", "")
if err != nil {
errs = append(errs, err)
return errs
}
defer os.RemoveAll(dir)
for w := 1; w <= workerLimit; w++ {
go func() {
for release := range jobQueue {
errs := []error{}
flags, err := flagsForRelease(helm, state.BaseChartPath, release)
if err != nil {
errs = append(errs, err)
}
for _, value := range additionalValues {
valfile, err := filepath.Abs(value)
if err != nil {
errs = append(errs, err)
}
if _, err := os.Stat(valfile); os.IsNotExist(err) {
errs = append(errs, err)
}
flags = append(flags, "--values", valfile)
}
chartPath := ""
if isLocalChart(release.Chart) {
chartPath = normalizeChart(state.BaseChartPath, release.Chart)
} else {
fetchFlags := []string{}
if release.Version != "" {
chartPath = path.Join(dir, release.Name, release.Version, release.Chart)
fetchFlags = append(fetchFlags, "--version", release.Version)
} else {
chartPath = path.Join(dir, release.Name, "latest", release.Chart)
}
// only fetch chart if it is not already fetched
if _, err := os.Stat(chartPath); os.IsNotExist(err) {
fetchFlags = append(fetchFlags, "--untar", "--untardir", chartPath)
if err := helm.Fetch(release.Chart, fetchFlags...); err != nil {
errs = append(errs, err)
}
}
chartPath = path.Join(chartPath, chartNameWithoutRepository(release.Chart))
}
// strip version from the slice returned from flagsForRelease
realFlags := []string{}
isVersion := false
for _, v := range flags {
if v == "--version" {
isVersion = true
} else if isVersion {
isVersion = false
} else {
realFlags = append(realFlags, v)
}
}
if len(errs) == 0 {
if err := helm.Lint(chartPath, realFlags...); err != nil {
errs = append(errs, err)
}
}
for _, err := range errs {
errQueue <- err
}
wgRelease.Done()
}
}()
}
wgError.Add(1)
go func() {
for err := range errQueue {
errs = append(errs, err)
}
wgError.Done()
}()
for i := 0; i < len(state.Releases); i++ {
jobQueue <- &state.Releases[i]
}
close(jobQueue)
wgRelease.Wait()
close(errQueue)
wgError.Wait()
if len(errs) != 0 {
return errs
}
return nil
}
func (state *HelmState) ReleaseStatuses(helm helmexec.Interface, workerLimit int) []error {
var errs []error
jobQueue := make(chan ReleaseSpec)
@ -505,6 +620,11 @@ func isLocalChart(chart string) bool {
return err == nil
}
func chartNameWithoutRepository(chart string) string {
chartSplit := strings.Split(chart, "/")
return chartSplit[len(chartSplit)-1]
}
func flagsForRelease(helm helmexec.Interface, basePath string, release *ReleaseSpec) ([]string, error) {
flags := []string{}
if release.Version != "" {

View File

@ -559,6 +559,12 @@ func (helm *mockHelmExec) TestRelease(name string, flags ...string) error {
helm.releases = append(helm.releases, mockRelease{name: name, flags: flags})
return nil
}
func (helm *mockHelmExec) Fetch(chart string, flags ...string) error {
return nil
}
func (helm *mockHelmExec) Lint(chart string, flags ...string) error {
return nil
}
func TestHelmState_SyncRepos(t *testing.T) {
tests := []struct {