Skip to content

Commit

Permalink
Add helmfile lint support (#162)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
jlyheden authored and mumoshu committed Jun 14, 2018
1 parent 2fba241 commit 6856c6e
Show file tree
Hide file tree
Showing 7 changed files with 198 additions and 0 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
12 changes: 12 additions & 0 deletions helmexec/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
20 changes: 20 additions & 0 deletions helmexec/exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
2 changes: 2 additions & 0 deletions helmexec/helmexec.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 33 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
Expand Down
120 changes: 120 additions & 0 deletions state/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"io/ioutil"
"os"
"path"
"path/filepath"
"strconv"
"strings"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 != "" {
Expand Down
6 changes: 6 additions & 0 deletions state/state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit 6856c6e

Please sign in to comment.