feat: add helm-unittest integration
This commit adds support for running helm-unittest on releases defined in helmfile.
Features:
- Add unittest command to run helm-unittest on releases
- Add unitTests field to release spec for specifying test directories
- Add helm-unittest plugin to init command
- Support for --values, --fail-fast, --color, --debug-plugin, --unittest-args flags
- Concurrent execution support
- Integration with needs/selectors like other commands
Example usage:
helmfile unittest -e test
helmfile unittest --fail-fast --color
helmfile unittest --values values.yaml
Example helmfile.yaml:
releases:
- name: myapp
chart: ./charts/myapp
unitTests:
- tests/
Signed-off-by: yxxhero <aiopsclub@163.com>
This commit is contained in:
parent
7500eef7c6
commit
e9069679f6
|
|
@ -105,6 +105,7 @@ func NewRootCmd(globalConfig *config.GlobalOptions) (*cobra.Command, error) {
|
|||
NewStatusCmd(globalImpl),
|
||||
NewShowDAGCmd(globalImpl),
|
||||
NewPrintEnvCmd(globalImpl),
|
||||
NewUnittestCmd(globalImpl),
|
||||
extension.NewVersionCobraCmd(
|
||||
versionOpts...,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/helmfile/helmfile/pkg/app"
|
||||
"github.com/helmfile/helmfile/pkg/config"
|
||||
)
|
||||
|
||||
func NewUnittestCmd(globalCfg *config.GlobalImpl) *cobra.Command {
|
||||
unittestOptions := config.NewUnittestOptions()
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "unittest",
|
||||
Short: "Run unit tests for charts",
|
||||
Long: "Run helm-unittest on releases defined in state file",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
unittestImpl := config.NewUnittestImpl(globalCfg, unittestOptions)
|
||||
err := config.NewCLIConfigImpl(unittestImpl.GlobalImpl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := unittestImpl.ValidateConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a := app.New(unittestImpl)
|
||||
return toCLIError(unittestImpl.GlobalImpl, a.Unittest(unittestImpl))
|
||||
},
|
||||
}
|
||||
|
||||
f := cmd.Flags()
|
||||
f.StringArrayVar(&unittestOptions.Values, "values", nil, "additional value files to be merged into the helm command --values flag")
|
||||
f.IntVar(&unittestOptions.Concurrency, "concurrency", 0, "maximum number of concurrent helm processes to run, 0 is unlimited")
|
||||
f.BoolVar(&unittestOptions.FailFast, "fail-fast", false, "fail fast on the first test failure")
|
||||
f.BoolVar(&unittestOptions.Color, "color", false, "output with color")
|
||||
f.BoolVar(&unittestOptions.DebugPlugin, "debug-plugin", false, "output plugin debug information")
|
||||
f.StringArrayVar(&unittestOptions.UnittestArgs, "unittest-args", nil, "additional arguments to pass to helm unittest")
|
||||
f.BoolVar(&unittestOptions.SkipNeeds, "skip-needs", true, `do not automatically include releases from the target release's "needs" when --selector/-l flag is provided. Does nothing when --selector/-l flag is not provided`)
|
||||
f.BoolVar(&unittestOptions.IncludeNeeds, "include-needs", false, `automatically include releases from the target release's "needs" when --selector/-l flag is provided. Does nothing when --selector/-l flag is not provided`)
|
||||
f.BoolVar(&unittestOptions.IncludeTransitiveNeeds, "include-transitive-needs", false, `like --include-needs, but also includes transitive needs (needs of needs). Does nothing when --selector/-l flag is not provided. Overrides exclusions of other selectors and conditions.`)
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
@ -344,6 +344,46 @@ func (a *App) Lint(c LintConfigProvider) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (a *App) Unittest(c UnittestConfigProvider) error {
|
||||
var deferredUnittestErrors []error
|
||||
|
||||
err := a.ForEachState(func(run *Run) (ok bool, errs []error) {
|
||||
var unittestErrs []error
|
||||
|
||||
prepErr := run.withPreparedCharts("unittest", state.ChartPrepareOptions{
|
||||
ForceDownload: true,
|
||||
SkipRepos: c.SkipRefresh() || c.SkipDeps(),
|
||||
SkipRefresh: c.SkipRefresh(),
|
||||
SkipDeps: c.SkipDeps(),
|
||||
SkipCleanup: c.SkipCleanup(),
|
||||
Concurrency: c.Concurrency(),
|
||||
IncludeTransitiveNeeds: c.IncludeNeeds(),
|
||||
}, func() {
|
||||
ok, unittestErrs, errs = a.unittest(run, c)
|
||||
})
|
||||
|
||||
if prepErr != nil {
|
||||
errs = append(errs, prepErr)
|
||||
}
|
||||
|
||||
if len(unittestErrs) > 0 {
|
||||
deferredUnittestErrors = append(deferredUnittestErrors, unittestErrs...)
|
||||
}
|
||||
|
||||
return
|
||||
}, c.IncludeTransitiveNeeds())
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(deferredUnittestErrors) > 0 {
|
||||
return &MultiError{Errors: deferredUnittestErrors}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) Fetch(c FetchConfigProvider) error {
|
||||
return a.ForEachState(func(run *Run) (ok bool, errs []error) {
|
||||
prepErr := run.withPreparedCharts("pull", state.ChartPrepareOptions{
|
||||
|
|
@ -1909,6 +1949,43 @@ func (a *App) lint(r *Run, c LintConfigProvider) (bool, []error, []error) {
|
|||
return ok, deferredLintErrs, errs
|
||||
}
|
||||
|
||||
func (a *App) unittest(r *Run, c UnittestConfigProvider) (bool, []error, []error) {
|
||||
var deferredUnittestErrs []error
|
||||
|
||||
ok, errs := a.withNeeds(r, c, true, func(st *state.HelmState) []error {
|
||||
helm := r.helm
|
||||
|
||||
args := GetArgs(c.Args(), st)
|
||||
|
||||
helm.SetExtraArgs()
|
||||
|
||||
if len(args) > 0 {
|
||||
helm.SetExtraArgs(args...)
|
||||
}
|
||||
|
||||
opts := &state.UnittestOpts{
|
||||
Color: c.Color(),
|
||||
DebugPlugin: c.DebugPlugin(),
|
||||
FailFast: c.FailFast(),
|
||||
UnittestArgs: c.UnittestArgs(),
|
||||
}
|
||||
unittestErrs := st.UninttestReleases(helm, c.Values(), c.Concurrency(), true, opts)
|
||||
if len(unittestErrs) == 1 {
|
||||
if err, ok := unittestErrs[0].(helmexec.ExitError); ok {
|
||||
if err.Code > 0 {
|
||||
deferredUnittestErrs = append(deferredUnittestErrs, err)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return unittestErrs
|
||||
})
|
||||
|
||||
return ok, deferredUnittestErrs, errs
|
||||
}
|
||||
|
||||
func (a *App) status(r *Run, c StatusesConfigProvider) (bool, []error) {
|
||||
st := r.state
|
||||
helm := r.helm
|
||||
|
|
|
|||
|
|
@ -2643,6 +2643,10 @@ func (helm *mockHelmExec) TemplateRelease(name, chart string, flags ...string) e
|
|||
return nil
|
||||
}
|
||||
|
||||
func (helm *mockHelmExec) UnittestRelease(context helmexec.HelmContext, name, chart string, flags ...string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (helm *mockHelmExec) ChartPull(chart string, path string, flags ...string) error {
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -128,6 +128,25 @@ type SyncConfigProvider interface {
|
|||
valuesControlMode
|
||||
}
|
||||
|
||||
type UnittestConfigProvider interface {
|
||||
Args() string
|
||||
Values() []string
|
||||
FailFast() bool
|
||||
Color() bool
|
||||
DebugPlugin() bool
|
||||
UnittestArgs() []string
|
||||
SkipNeeds() bool
|
||||
IncludeNeeds() bool
|
||||
IncludeTransitiveNeeds() bool
|
||||
EnforceNeedsAreInstalled() bool
|
||||
SkipDeps() bool
|
||||
SkipRefresh() bool
|
||||
SkipCleanup() bool
|
||||
|
||||
concurrencyConfig
|
||||
DAGConfig
|
||||
}
|
||||
|
||||
type DiffConfigProvider interface {
|
||||
Args() string
|
||||
PostRenderer() string
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ const (
|
|||
HelmSecretsRecommendedVersion = "v4.7.4" // v4.7.0+ works with both Helm 3 (single plugin) and Helm 4 (split plugin architecture)
|
||||
HelmGitRecommendedVersion = "v1.3.0"
|
||||
HelmS3RecommendedVersion = "v0.16.3"
|
||||
HelmUnittestVersion = "v0.5.1"
|
||||
HelmInstallCommand = "https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3" // Default to Helm 3 script for compatibility
|
||||
)
|
||||
|
||||
|
|
@ -54,6 +55,11 @@ var (
|
|||
version: HelmGitRecommendedVersion,
|
||||
repo: "https://github.com/aslafy-z/helm-git.git",
|
||||
},
|
||||
{
|
||||
name: "unittest",
|
||||
version: HelmUnittestVersion,
|
||||
repo: "https://github.com/helm-unittest/helm-unittest.git",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,85 @@
|
|||
package config
|
||||
|
||||
type UnittestOptions struct {
|
||||
Values []string
|
||||
FailFast bool
|
||||
Color bool
|
||||
DebugPlugin bool
|
||||
UnittestArgs []string
|
||||
Concurrency int
|
||||
SkipNeeds bool
|
||||
IncludeNeeds bool
|
||||
IncludeTransitiveNeeds bool
|
||||
}
|
||||
|
||||
func NewUnittestOptions() *UnittestOptions {
|
||||
return &UnittestOptions{}
|
||||
}
|
||||
|
||||
type UnittestImpl struct {
|
||||
*GlobalImpl
|
||||
*UnittestOptions
|
||||
}
|
||||
|
||||
func NewUnittestImpl(g *GlobalImpl, t *UnittestOptions) *UnittestImpl {
|
||||
return &UnittestImpl{
|
||||
GlobalImpl: g,
|
||||
UnittestOptions: t,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *UnittestImpl) Values() []string {
|
||||
return t.UnittestOptions.Values
|
||||
}
|
||||
|
||||
func (t *UnittestImpl) Concurrency() int {
|
||||
return t.UnittestOptions.Concurrency
|
||||
}
|
||||
|
||||
func (t *UnittestImpl) FailFast() bool {
|
||||
return t.UnittestOptions.FailFast
|
||||
}
|
||||
|
||||
func (t *UnittestImpl) Color() bool {
|
||||
return t.UnittestOptions.Color
|
||||
}
|
||||
|
||||
func (t *UnittestImpl) DebugPlugin() bool {
|
||||
return t.UnittestOptions.DebugPlugin
|
||||
}
|
||||
|
||||
func (t *UnittestImpl) UnittestArgs() []string {
|
||||
return t.UnittestOptions.UnittestArgs
|
||||
}
|
||||
|
||||
func (t *UnittestImpl) Args() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (t *UnittestImpl) SkipNeeds() bool {
|
||||
return t.UnittestOptions.SkipNeeds
|
||||
}
|
||||
|
||||
func (t *UnittestImpl) IncludeNeeds() bool {
|
||||
return t.UnittestOptions.IncludeNeeds
|
||||
}
|
||||
|
||||
func (t *UnittestImpl) IncludeTransitiveNeeds() bool {
|
||||
return t.UnittestOptions.IncludeTransitiveNeeds
|
||||
}
|
||||
|
||||
func (t *UnittestImpl) SkipDeps() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *UnittestImpl) SkipRefresh() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *UnittestImpl) SkipCleanup() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *UnittestImpl) EnforceNeedsAreInstalled() bool {
|
||||
return false
|
||||
}
|
||||
|
|
@ -224,6 +224,9 @@ func (helm *Helm) TemplateRelease(name, chart string, flags ...string) error {
|
|||
helm.Templated = append(helm.Templated, Release{Name: name, Flags: flags})
|
||||
return nil
|
||||
}
|
||||
func (helm *Helm) UnittestRelease(context helmexec.HelmContext, name, chart string, flags ...string) error {
|
||||
return nil
|
||||
}
|
||||
func (helm *Helm) ChartPull(chart string, path string, flags ...string) error {
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -649,6 +649,17 @@ func (helm *execer) TemplateRelease(name string, chart string, flags ...string)
|
|||
return err
|
||||
}
|
||||
|
||||
func (helm *execer) UnittestRelease(context HelmContext, name, chart string, flags ...string) error {
|
||||
helm.logger.Infof("Running unittest for release=%v, chart=%v", name, redactedURL(chart))
|
||||
preArgs := make([]string, 0)
|
||||
env := make(map[string]string)
|
||||
var overrideEnableLiveOutput *bool = nil
|
||||
|
||||
out, err := helm.exec(append(append(preArgs, "unittest", chart), flags...), env, overrideEnableLiveOutput)
|
||||
helm.write(context.Writer, out)
|
||||
return err
|
||||
}
|
||||
|
||||
func (helm *execer) DiffRelease(context HelmContext, name, chart, namespace string, suppressDiff bool, flags ...string) error {
|
||||
diffMsg := fmt.Sprintf("Comparing release=%v, chart=%v, namespace=%v\n", name, redactedURL(chart), namespace)
|
||||
if context.Writer != nil && !suppressDiff {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ type Interface interface {
|
|||
SyncRelease(context HelmContext, name, chart, namespace string, flags ...string) error
|
||||
DiffRelease(context HelmContext, name, chart, namespace string, suppressDiff bool, flags ...string) error
|
||||
TemplateRelease(name, chart string, flags ...string) error
|
||||
UnittestRelease(context HelmContext, name, chart string, flags ...string) error
|
||||
Fetch(chart string, flags ...string) error
|
||||
ChartPull(chart string, path string, flags ...string) error
|
||||
ChartExport(chart string, path string) error
|
||||
|
|
|
|||
|
|
@ -442,6 +442,9 @@ type ReleaseSpec struct {
|
|||
SyncReleaseLabels *bool `yaml:"syncReleaseLabels,omitempty"`
|
||||
// TakeOwnership is true if the release should take ownership of the resources
|
||||
TakeOwnership *bool `yaml:"takeOwnership,omitempty"`
|
||||
|
||||
// UnitTests is a list of paths to unittest directories for this release
|
||||
UnitTests []string `yaml:"unitTests,omitempty"`
|
||||
}
|
||||
|
||||
func (r *Inherits) UnmarshalYAML(unmarshal func(any) error) error {
|
||||
|
|
@ -2166,6 +2169,95 @@ func (st *HelmState) LintReleases(helm helmexec.Interface, additionalValues []st
|
|||
return nil
|
||||
}
|
||||
|
||||
type UnittestOpts struct {
|
||||
Color bool
|
||||
DebugPlugin bool
|
||||
FailFast bool
|
||||
UnittestArgs []string
|
||||
}
|
||||
|
||||
func (o *UnittestOpts) Apply(opts *UnittestOpts) {
|
||||
*opts = *o
|
||||
}
|
||||
|
||||
type UnittestOpt interface{ Apply(*UnittestOpts) }
|
||||
|
||||
// UninttestReleases wrapper for executing helm unittest on the releases
|
||||
func (st *HelmState) UninttestReleases(helm helmexec.Interface, additionalValues []string, workerLimit int, triggerCleanupEvents bool, opt ...UnittestOpt) []error {
|
||||
opts := &UnittestOpts{}
|
||||
for _, o := range opt {
|
||||
o.Apply(opts)
|
||||
}
|
||||
|
||||
helm.SetExtraArgs()
|
||||
|
||||
errs := []error{}
|
||||
|
||||
for i := range st.Releases {
|
||||
release := st.Releases[i]
|
||||
|
||||
if !release.Desired() {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(release.UnitTests) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
flags, files, err := st.flagsForUnittest(helm, &release, 0)
|
||||
|
||||
defer st.removeFiles(files)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
flags = append(flags, opts.UnittestArgs...)
|
||||
|
||||
if opts.Color {
|
||||
flags = append(flags, "--color")
|
||||
}
|
||||
|
||||
if opts.DebugPlugin {
|
||||
flags = append(flags, "--debug-plugin")
|
||||
}
|
||||
|
||||
if opts.FailFast {
|
||||
flags = append(flags, "--fail-fast")
|
||||
}
|
||||
|
||||
if len(errs) == 0 {
|
||||
if err := helm.UnittestRelease(st.createHelmContext(&release, 0), release.Name, release.ChartPathOrName(), flags...); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
if triggerCleanupEvents {
|
||||
if _, err := st.TriggerCleanupEvent(&release, "unittest"); err != nil {
|
||||
st.logger.Warnf("warn: %v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) != 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type diffResult struct {
|
||||
release *ReleaseSpec
|
||||
err *ReleaseError
|
||||
|
|
@ -3601,6 +3693,10 @@ func (st *HelmState) flagsForLint(helm helmexec.Interface, release *ReleaseSpec,
|
|||
return st.appendHelmXFlags(flags, release), files, nil
|
||||
}
|
||||
|
||||
func (st *HelmState) flagsForUnittest(helm helmexec.Interface, release *ReleaseSpec, workerIndex int) ([]string, []string, error) {
|
||||
return st.namespaceAndValuesFlags(helm, release, workerIndex)
|
||||
}
|
||||
|
||||
func (st *HelmState) newReleaseTemplateData(release *ReleaseSpec) releaseTemplateData {
|
||||
vals := st.Values()
|
||||
templateData := st.createReleaseTemplateData(release, vals)
|
||||
|
|
|
|||
|
|
@ -61,6 +61,10 @@ func (helm *noCallHelmExec) TemplateRelease(name, chart string, flags ...string)
|
|||
helm.doPanic()
|
||||
return nil
|
||||
}
|
||||
func (helm *noCallHelmExec) UnittestRelease(context helmexec.HelmContext, name, chart string, flags ...string) error {
|
||||
helm.doPanic()
|
||||
return nil
|
||||
}
|
||||
func (helm *noCallHelmExec) ChartPull(chart string, path string, flags ...string) error {
|
||||
helm.doPanic()
|
||||
return nil
|
||||
|
|
|
|||
Loading…
Reference in New Issue