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:
parent
2fba241122
commit
6856c6e979
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
33
main.go
|
|
@ -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)",
|
||||
|
|
|
|||
120
state/state.go
120
state/state.go
|
|
@ -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 != "" {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue