feat: add `helmfile unittest` command for helm-unittest integration (#2400)
Adds a new `helmfile unittest` command that integrates the helm-unittest plugin, allowing users to define unit test paths per release and run them via helmfile. Closes #2376 Signed-off-by: Aditya Menon <amenon@canarytechnologies.com>
This commit is contained in:
parent
503c397810
commit
0129681222
|
|
@ -99,6 +99,7 @@ func NewRootCmd(globalConfig *config.GlobalOptions) (*cobra.Command, error) {
|
|||
NewLintCmd(globalImpl),
|
||||
NewWriteValuesCmd(globalImpl),
|
||||
NewTestCmd(globalImpl),
|
||||
NewUnittestCmd(globalImpl),
|
||||
NewTemplateCmd(globalImpl),
|
||||
NewSyncCmd(globalImpl),
|
||||
NewDiffCmd(globalImpl),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/helmfile/helmfile/pkg/app"
|
||||
"github.com/helmfile/helmfile/pkg/config"
|
||||
)
|
||||
|
||||
// NewUnittestCmd returns unittest subcmd
|
||||
func NewUnittestCmd(globalCfg *config.GlobalImpl) *cobra.Command {
|
||||
unittestOptions := config.NewUnittestOptions()
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "unittest",
|
||||
Short: "Unit test charts from state file using helm-unittest plugin",
|
||||
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.IntVar(&unittestOptions.Concurrency, "concurrency", 0, "maximum number of concurrent helm processes to run, 0 is unlimited")
|
||||
f.StringVar(&globalCfg.GlobalOptions.Args, "args", "", "pass args to helm exec")
|
||||
f.StringArrayVar(&unittestOptions.Set, "set", nil, "additional values to be merged into the helm command --set flag")
|
||||
f.StringArrayVar(&unittestOptions.Values, "values", nil, "additional value files to be merged into the helm command --values flag")
|
||||
f.BoolVar(&unittestOptions.FailFast, "fail-fast", false, "fail fast on the first test failure")
|
||||
f.BoolVar(&unittestOptions.Color, "color", false, "enforce colored output even when stdout is not a tty (ignored on Helm 4 due to flag parsing issues)")
|
||||
f.BoolVar(&unittestOptions.DebugPlugin, "debug-plugin", false, "enable verbose output from the helm-unittest plugin")
|
||||
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. Defaults to true when --include-needs or --include-transitive-needs 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
|
||||
}
|
||||
|
|
@ -380,6 +380,11 @@ releases:
|
|||
- "version"
|
||||
# syncReleaseLabels is a list of labels to be added to the release when syncing.
|
||||
syncReleaseLabels: false
|
||||
# unitTests is a list of test file or directory paths for helm-unittest integration.
|
||||
# When specified, `helmfile unittest` will run `helm unittest` with the merged values and these test paths.
|
||||
# Requires the helm-unittest plugin: https://github.com/helm-unittest/helm-unittest
|
||||
unitTests:
|
||||
- tests/vault
|
||||
|
||||
|
||||
# Local chart example
|
||||
|
|
@ -619,6 +624,7 @@ Available Commands:
|
|||
sync Sync releases defined in state file
|
||||
template Template releases defined in state file
|
||||
test Test charts from state file (helm test)
|
||||
unittest Unit test charts from state file using helm-unittest plugin
|
||||
version Print the CLI version
|
||||
write-values Write values files for releases. Similar to `helmfile template`, write values files instead of manifests.
|
||||
|
||||
|
|
@ -787,6 +793,64 @@ Use `--cleanup` to delete pods upon completion.
|
|||
|
||||
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.
|
||||
|
||||
### unittest
|
||||
|
||||
The `helmfile unittest` sub-command runs `helm unittest` (from the [helm-unittest plugin](https://github.com/helm-unittest/helm-unittest)) on releases that have `unitTests` defined. It automatically generates the final merged values files for each release and passes them to `helm unittest`.
|
||||
|
||||
This requires the `helm-unittest` plugin to be installed. You can install it with:
|
||||
|
||||
```bash
|
||||
helm plugin install https://github.com/helm-unittest/helm-unittest
|
||||
```
|
||||
|
||||
Releases without `unitTests` defined are skipped. Non-local charts will be fetched into a temporary folder which will be deleted once the task is completed.
|
||||
|
||||
Example helmfile configuration:
|
||||
|
||||
```yaml
|
||||
releases:
|
||||
- name: my-app
|
||||
chart: ./charts/my-app
|
||||
values:
|
||||
- values.yaml
|
||||
unitTests:
|
||||
- tests
|
||||
```
|
||||
|
||||
The `unitTests` paths are relative to the chart directory and follow helm-unittest conventions.
|
||||
If a path does not contain glob characters, it is treated as a directory and `/*_test.yaml` is appended automatically.
|
||||
You can also specify explicit glob patterns (e.g., `tests/**/*_test.yaml`).
|
||||
|
||||
Running `helmfile unittest` will:
|
||||
|
||||
1. Merge all values files defined for the release
|
||||
2. Run `helm unittest ./charts/my-app --values <merged-values> --file tests/*_test.yaml`
|
||||
|
||||
You can pass additional flags:
|
||||
|
||||
```bash
|
||||
# Run with additional values
|
||||
helmfile unittest --values extra-values.yaml
|
||||
|
||||
# Run with --set overrides
|
||||
helmfile unittest --set key=value
|
||||
|
||||
# Target specific releases
|
||||
helmfile unittest --selector name=my-app
|
||||
|
||||
# Fail fast on first test failure
|
||||
helmfile unittest --fail-fast
|
||||
|
||||
# Enable colored output (Helm 3 only; ignored on Helm 4 due to flag parsing issues)
|
||||
helmfile unittest --color
|
||||
|
||||
# Enable verbose plugin output
|
||||
helmfile unittest --debug-plugin
|
||||
|
||||
# Pass extra arguments to helm unittest
|
||||
helmfile unittest --args "--strict"
|
||||
```
|
||||
|
||||
### fetch
|
||||
|
||||
The `helmfile fetch` sub-command downloads or copies local charts to a local directory for debug purpose. The local directory
|
||||
|
|
|
|||
|
|
@ -344,6 +344,47 @@ 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
|
||||
|
||||
// helm unittest needs local charts, so force download
|
||||
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.IncludeTransitiveNeeds(),
|
||||
}, 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 +1950,45 @@ 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, false, func(st *state.HelmState) []error {
|
||||
helm := r.helm
|
||||
|
||||
args := GetArgs(c.Args(), st)
|
||||
|
||||
// Reset the extra args if already set, not to break `helm fetch` by adding the args intended for `unittest`
|
||||
helm.SetExtraArgs()
|
||||
|
||||
if len(args) > 0 {
|
||||
helm.SetExtraArgs(args...)
|
||||
}
|
||||
|
||||
opts := &state.UnittestOpts{
|
||||
Set: c.Set(),
|
||||
SkipCleanup: c.SkipCleanup(),
|
||||
FailFast: c.FailFast(),
|
||||
Color: c.Color(),
|
||||
DebugPlugin: c.DebugPlugin(),
|
||||
}
|
||||
unittestErrs := st.UnittestReleases(helm, c.Values(), args, c.Concurrency(), 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
|
||||
|
|
|
|||
|
|
@ -2354,6 +2354,8 @@ type applyConfig struct {
|
|||
suppressDiff bool
|
||||
noColor bool
|
||||
color bool
|
||||
failFast bool
|
||||
debugPlugin bool
|
||||
context int
|
||||
diffOutput string
|
||||
concurrency int
|
||||
|
|
@ -2476,6 +2478,14 @@ func (a applyConfig) Color() bool {
|
|||
return a.color
|
||||
}
|
||||
|
||||
func (a applyConfig) FailFast() bool {
|
||||
return a.failFast
|
||||
}
|
||||
|
||||
func (a applyConfig) DebugPlugin() bool {
|
||||
return a.debugPlugin
|
||||
}
|
||||
|
||||
func (a applyConfig) NoColor() bool {
|
||||
return a.noColor
|
||||
}
|
||||
|
|
@ -2710,6 +2720,9 @@ func (helm *mockHelmExec) Fetch(chart string, flags ...string) error {
|
|||
func (helm *mockHelmExec) Lint(name, chart string, flags ...string) error {
|
||||
return nil
|
||||
}
|
||||
func (helm *mockHelmExec) Unittest(name, chart string, flags ...string) error {
|
||||
return nil
|
||||
}
|
||||
func (helm *mockHelmExec) IsHelm3() bool {
|
||||
return !exectest.IsHelm4Enabled()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,256 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/helmfile/vals"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/helmfile/helmfile/pkg/exectest"
|
||||
ffs "github.com/helmfile/helmfile/pkg/filesystem"
|
||||
"github.com/helmfile/helmfile/pkg/helmexec"
|
||||
)
|
||||
|
||||
func TestUnittest(t *testing.T) {
|
||||
type fields struct {
|
||||
skipNeeds bool
|
||||
includeNeeds bool
|
||||
includeTransitiveNeeds bool
|
||||
failFast bool
|
||||
color bool
|
||||
debugPlugin bool
|
||||
}
|
||||
|
||||
type testcase struct {
|
||||
fields fields
|
||||
ns string
|
||||
error string
|
||||
selectors []string
|
||||
unittested []exectest.Release
|
||||
}
|
||||
|
||||
check := func(t *testing.T, tc testcase) {
|
||||
t.Helper()
|
||||
|
||||
wantUnittests := tc.unittested
|
||||
|
||||
var helm = &exectest.Helm{
|
||||
FailOnUnexpectedList: true,
|
||||
FailOnUnexpectedDiff: true,
|
||||
Helm4: exectest.IsHelm4Enabled(),
|
||||
Helm3: !exectest.IsHelm4Enabled(),
|
||||
DiffMutex: &sync.Mutex{},
|
||||
ChartsMutex: &sync.Mutex{},
|
||||
ReleasesMutex: &sync.Mutex{},
|
||||
}
|
||||
|
||||
bs := runWithLogCapture(t, "debug", func(t *testing.T, logger *zap.SugaredLogger) {
|
||||
t.Helper()
|
||||
|
||||
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error creating vals runtime: %v", err)
|
||||
}
|
||||
|
||||
files := map[string]string{
|
||||
"/path/to/helmfile.yaml": `
|
||||
releases:
|
||||
- name: logging
|
||||
chart: incubator/raw
|
||||
namespace: kube-system
|
||||
unitTests:
|
||||
- tests/logging
|
||||
|
||||
- name: kubernetes-external-secrets
|
||||
chart: incubator/raw
|
||||
namespace: kube-system
|
||||
needs:
|
||||
- kube-system/logging
|
||||
unitTests:
|
||||
- tests/secrets
|
||||
|
||||
- name: external-secrets
|
||||
chart: incubator/raw
|
||||
namespace: default
|
||||
labels:
|
||||
app: test
|
||||
needs:
|
||||
- kube-system/kubernetes-external-secrets
|
||||
unitTests:
|
||||
- tests/external
|
||||
|
||||
- name: my-release
|
||||
chart: incubator/raw
|
||||
namespace: default
|
||||
labels:
|
||||
app: test
|
||||
needs:
|
||||
- default/external-secrets
|
||||
unitTests:
|
||||
- tests/myrelease
|
||||
|
||||
- name: no-tests
|
||||
chart: incubator/raw
|
||||
namespace: default
|
||||
`,
|
||||
}
|
||||
|
||||
app := appWithFs(&App{
|
||||
OverrideHelmBinary: DefaultHelmBinary,
|
||||
fs: ffs.DefaultFileSystem(),
|
||||
OverrideKubeContext: "default",
|
||||
DisableKubeVersionAutoDetection: true,
|
||||
Env: "default",
|
||||
Logger: logger,
|
||||
helms: map[helmKey]helmexec.Interface{
|
||||
createHelmKey("helm", "default"): helm,
|
||||
},
|
||||
valsRuntime: valsRuntime,
|
||||
}, files)
|
||||
|
||||
if tc.ns != "" {
|
||||
app.Namespace = tc.ns
|
||||
}
|
||||
|
||||
if tc.selectors != nil {
|
||||
app.Selectors = tc.selectors
|
||||
}
|
||||
|
||||
unittestErr := app.Unittest(applyConfig{
|
||||
concurrency: 1,
|
||||
logger: logger,
|
||||
skipNeeds: tc.fields.skipNeeds,
|
||||
includeNeeds: tc.fields.includeNeeds,
|
||||
includeTransitiveNeeds: tc.fields.includeTransitiveNeeds,
|
||||
failFast: tc.fields.failFast,
|
||||
color: tc.fields.color,
|
||||
debugPlugin: tc.fields.debugPlugin,
|
||||
})
|
||||
|
||||
var gotErr string
|
||||
if unittestErr != nil {
|
||||
gotErr = unittestErr.Error()
|
||||
}
|
||||
|
||||
if d := cmp.Diff(tc.error, gotErr); d != "" {
|
||||
t.Fatalf("unexpected error: want (-), got (+): %s", d)
|
||||
}
|
||||
|
||||
require.Equal(t, wantUnittests, helm.Unittested)
|
||||
})
|
||||
|
||||
testNameComponents := strings.Split(t.Name(), "/")
|
||||
testBaseName := strings.ToLower(
|
||||
strings.ReplaceAll(
|
||||
testNameComponents[len(testNameComponents)-1],
|
||||
" ",
|
||||
"_",
|
||||
),
|
||||
)
|
||||
wantLogFileDir := filepath.Join("testdata", "app_unittest_test")
|
||||
snapshotName := testBaseName
|
||||
if exectest.IsHelm4Enabled() {
|
||||
if _, err := os.Stat(filepath.Join(wantLogFileDir, testBaseName+"_helm4")); err == nil {
|
||||
snapshotName = testBaseName + "_helm4"
|
||||
}
|
||||
}
|
||||
wantLogFile := filepath.Join(wantLogFileDir, snapshotName)
|
||||
wantLogData, err := os.ReadFile(wantLogFile)
|
||||
updateLogFile := err != nil
|
||||
wantLog := string(wantLogData)
|
||||
gotLog := bs.String()
|
||||
if updateLogFile {
|
||||
if err := os.MkdirAll(wantLogFileDir, 0755); err != nil {
|
||||
t.Fatalf("unable to create directory %q: %v", wantLogFileDir, err)
|
||||
}
|
||||
if err := os.WriteFile(wantLogFile, bs.Bytes(), 0644); err != nil {
|
||||
t.Fatalf("unable to update unittest log snapshot: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
assert.Equal(t, wantLog, gotLog)
|
||||
}
|
||||
|
||||
t.Run("unittest all releases with unitTests", func(t *testing.T) {
|
||||
check(t, testcase{
|
||||
unittested: []exectest.Release{
|
||||
{Name: "logging", Flags: []string{"--namespace", "kube-system", "--file", "tests/logging/*_test.yaml"}},
|
||||
{Name: "kubernetes-external-secrets", Flags: []string{"--namespace", "kube-system", "--file", "tests/secrets/*_test.yaml"}},
|
||||
{Name: "external-secrets", Flags: []string{"--namespace", "default", "--file", "tests/external/*_test.yaml"}},
|
||||
{Name: "my-release", Flags: []string{"--namespace", "default", "--file", "tests/myrelease/*_test.yaml"}},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("with dedicated flags", func(t *testing.T) {
|
||||
// --color is skipped on Helm 4 due to flag parsing issues
|
||||
expectedFlags := []string{"--namespace", "kube-system", "--failfast"}
|
||||
if !exectest.IsHelm4Enabled() {
|
||||
expectedFlags = append(expectedFlags, "--color")
|
||||
}
|
||||
expectedFlags = append(expectedFlags, "--debugPlugin", "--file", "tests/logging/*_test.yaml")
|
||||
|
||||
check(t, testcase{
|
||||
fields: fields{
|
||||
failFast: true,
|
||||
color: true,
|
||||
debugPlugin: true,
|
||||
},
|
||||
selectors: []string{"name=logging"},
|
||||
unittested: []exectest.Release{
|
||||
{Name: "logging", Flags: expectedFlags},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("skip-needs", func(t *testing.T) {
|
||||
check(t, testcase{
|
||||
fields: fields{
|
||||
skipNeeds: true,
|
||||
},
|
||||
selectors: []string{"app=test"},
|
||||
unittested: []exectest.Release{
|
||||
{Name: "external-secrets", Flags: []string{"--namespace", "default", "--file", "tests/external/*_test.yaml"}},
|
||||
{Name: "my-release", Flags: []string{"--namespace", "default", "--file", "tests/myrelease/*_test.yaml"}},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("include-needs", func(t *testing.T) {
|
||||
check(t, testcase{
|
||||
fields: fields{
|
||||
skipNeeds: false,
|
||||
includeNeeds: true,
|
||||
},
|
||||
selectors: []string{"app=test"},
|
||||
unittested: []exectest.Release{
|
||||
{Name: "logging", Flags: []string{"--namespace", "kube-system", "--file", "tests/logging/*_test.yaml"}},
|
||||
{Name: "kubernetes-external-secrets", Flags: []string{"--namespace", "kube-system", "--file", "tests/secrets/*_test.yaml"}},
|
||||
{Name: "external-secrets", Flags: []string{"--namespace", "default", "--file", "tests/external/*_test.yaml"}},
|
||||
{Name: "my-release", Flags: []string{"--namespace", "default", "--file", "tests/myrelease/*_test.yaml"}},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("release without unitTests is skipped", func(t *testing.T) {
|
||||
check(t, testcase{
|
||||
selectors: []string{"name=no-tests"},
|
||||
unittested: nil,
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("bad selector", func(t *testing.T) {
|
||||
check(t, testcase{
|
||||
selectors: []string{"app=test_non_existent"},
|
||||
unittested: nil,
|
||||
error: "err: no releases found that matches specified selector(app=test_non_existent) and environment(default), in any helmfile",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
@ -207,6 +207,23 @@ type LintConfigProvider interface {
|
|||
concurrencyConfig
|
||||
}
|
||||
|
||||
type UnittestConfigProvider interface {
|
||||
Args() string
|
||||
|
||||
Values() []string
|
||||
Set() []string
|
||||
FailFast() bool
|
||||
Color() bool
|
||||
DebugPlugin() bool
|
||||
SkipDeps() bool
|
||||
SkipRefresh() bool
|
||||
SkipCleanup() bool
|
||||
|
||||
DAGConfig
|
||||
|
||||
concurrencyConfig
|
||||
}
|
||||
|
||||
type FetchConfigProvider interface {
|
||||
SkipDeps() bool
|
||||
SkipRefresh() bool
|
||||
|
|
|
|||
|
|
@ -18,13 +18,14 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
HelmRequiredVersion = "v3.18.6" // Minimum required version (supports Helm 3.x and 4.x)
|
||||
HelmDiffRecommendedVersion = "v3.14.1"
|
||||
HelmRecommendedVersion = "v4.1.0" // Recommended to use latest Helm 4
|
||||
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"
|
||||
HelmInstallCommand = "https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3" // Default to Helm 3 script for compatibility
|
||||
HelmRequiredVersion = "v3.18.6" // Minimum required version (supports Helm 3.x and 4.x)
|
||||
HelmDiffRecommendedVersion = "v3.14.1"
|
||||
HelmRecommendedVersion = "v4.1.0" // Recommended to use latest Helm 4
|
||||
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"
|
||||
HelmUnittestRecommendedVersion = "v1.0.3"
|
||||
HelmInstallCommand = "https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3" // Default to Helm 3 script for compatibility
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -54,6 +55,11 @@ var (
|
|||
version: HelmGitRecommendedVersion,
|
||||
repo: "https://github.com/aslafy-z/helm-git.git",
|
||||
},
|
||||
{
|
||||
name: "unittest",
|
||||
version: HelmUnittestRecommendedVersion,
|
||||
repo: "https://github.com/helm-unittest/helm-unittest",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
processing file "helmfile.yaml" in directory "."
|
||||
changing working directory to "/path/to"
|
||||
merged environment: &{default map[] map[] map[]}
|
||||
0 release(s) matching app=test_non_existent found in helmfile.yaml
|
||||
|
||||
changing working directory back to "/path/to"
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
processing file "helmfile.yaml" in directory "."
|
||||
changing working directory to "/path/to"
|
||||
merged environment: &{default map[] map[] map[]}
|
||||
2 release(s) matching app=test found in helmfile.yaml
|
||||
|
||||
processing 4 groups of releases in this order:
|
||||
GROUP RELEASES
|
||||
1 default/kube-system/logging
|
||||
2 default/kube-system/kubernetes-external-secrets
|
||||
3 default/default/external-secrets
|
||||
4 default/default/my-release
|
||||
|
||||
processing releases in group 1/4: default/kube-system/logging
|
||||
processing releases in group 2/4: default/kube-system/kubernetes-external-secrets
|
||||
processing releases in group 3/4: default/default/external-secrets
|
||||
processing releases in group 4/4: default/default/my-release
|
||||
changing working directory back to "/path/to"
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
processing file "helmfile.yaml" in directory "."
|
||||
changing working directory to "/path/to"
|
||||
merged environment: &{default map[] map[] map[]}
|
||||
1 release(s) matching name=no-tests found in helmfile.yaml
|
||||
|
||||
processing 1 groups of releases in this order:
|
||||
GROUP RELEASES
|
||||
1 default/default/no-tests
|
||||
|
||||
processing releases in group 1/1: default/default/no-tests
|
||||
changing working directory back to "/path/to"
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
processing file "helmfile.yaml" in directory "."
|
||||
changing working directory to "/path/to"
|
||||
merged environment: &{default map[] map[] map[]}
|
||||
2 release(s) matching app=test found in helmfile.yaml
|
||||
|
||||
processing 2 groups of releases in this order:
|
||||
GROUP RELEASES
|
||||
1 default/default/external-secrets
|
||||
2 default/default/my-release
|
||||
|
||||
processing releases in group 1/2: default/default/external-secrets
|
||||
processing releases in group 2/2: default/default/my-release
|
||||
changing working directory back to "/path/to"
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
processing file "helmfile.yaml" in directory "."
|
||||
changing working directory to "/path/to"
|
||||
merged environment: &{default map[] map[] map[]}
|
||||
5 release(s) found in helmfile.yaml
|
||||
|
||||
processing 4 groups of releases in this order:
|
||||
GROUP RELEASES
|
||||
1 default/kube-system/logging, default/default/no-tests
|
||||
2 default/kube-system/kubernetes-external-secrets
|
||||
3 default/default/external-secrets
|
||||
4 default/default/my-release
|
||||
|
||||
processing releases in group 1/4: default/kube-system/logging, default/default/no-tests
|
||||
processing releases in group 2/4: default/kube-system/kubernetes-external-secrets
|
||||
processing releases in group 3/4: default/default/external-secrets
|
||||
processing releases in group 4/4: default/default/my-release
|
||||
changing working directory back to "/path/to"
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
processing file "helmfile.yaml" in directory "."
|
||||
changing working directory to "/path/to"
|
||||
merged environment: &{default map[] map[] map[]}
|
||||
1 release(s) matching name=logging found in helmfile.yaml
|
||||
|
||||
processing 1 groups of releases in this order:
|
||||
GROUP RELEASES
|
||||
1 default/kube-system/logging
|
||||
|
||||
processing releases in group 1/1: default/kube-system/logging
|
||||
changing working directory back to "/path/to"
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
processing file "helmfile.yaml" in directory "."
|
||||
changing working directory to "/path/to"
|
||||
merged environment: &{default map[] map[] map[]}
|
||||
1 release(s) matching name=logging found in helmfile.yaml
|
||||
|
||||
processing 1 groups of releases in this order:
|
||||
GROUP RELEASES
|
||||
1 default/kube-system/logging
|
||||
|
||||
processing releases in group 1/1: default/kube-system/logging
|
||||
warn: --color flag is not supported with Helm 4 due to flag parsing issues, ignoring
|
||||
|
||||
changing working directory back to "/path/to"
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
package config
|
||||
|
||||
// UnittestOptions is the options for the unittest command
|
||||
type UnittestOptions struct {
|
||||
// Concurrency is the maximum number of concurrent helm processes to run, 0 is unlimited
|
||||
Concurrency int
|
||||
// Set is the set flags to pass to helm unittest
|
||||
Set []string
|
||||
// Values is the values flags to pass to helm unittest
|
||||
Values []string
|
||||
// FailFast causes helm-unittest to quit immediately when a test fails
|
||||
FailFast bool
|
||||
// Color enforces colored output even when stdout is not a tty
|
||||
Color bool
|
||||
// DebugPlugin enables verbose output from the helm-unittest plugin
|
||||
DebugPlugin bool
|
||||
// SkipNeeds is the skip needs flag
|
||||
SkipNeeds bool
|
||||
// IncludeNeeds is the include needs flag
|
||||
IncludeNeeds bool
|
||||
// IncludeTransitiveNeeds is the include transitive needs flag
|
||||
IncludeTransitiveNeeds bool
|
||||
}
|
||||
|
||||
// NewUnittestOptions creates a new UnittestOptions
|
||||
func NewUnittestOptions() *UnittestOptions {
|
||||
return &UnittestOptions{}
|
||||
}
|
||||
|
||||
// UnittestImpl is impl for UnittestOptions
|
||||
type UnittestImpl struct {
|
||||
*GlobalImpl
|
||||
*UnittestOptions
|
||||
}
|
||||
|
||||
// NewUnittestImpl creates a new UnittestImpl
|
||||
func NewUnittestImpl(g *GlobalImpl, u *UnittestOptions) *UnittestImpl {
|
||||
return &UnittestImpl{
|
||||
GlobalImpl: g,
|
||||
UnittestOptions: u,
|
||||
}
|
||||
}
|
||||
|
||||
// Concurrency returns the concurrency
|
||||
func (u *UnittestImpl) Concurrency() int {
|
||||
return u.UnittestOptions.Concurrency
|
||||
}
|
||||
|
||||
// Set returns the Set
|
||||
func (u *UnittestImpl) Set() []string {
|
||||
return u.UnittestOptions.Set
|
||||
}
|
||||
|
||||
// Values returns the Values
|
||||
func (u *UnittestImpl) Values() []string {
|
||||
return u.UnittestOptions.Values
|
||||
}
|
||||
|
||||
// FailFast returns the fail fast flag
|
||||
func (u *UnittestImpl) FailFast() bool {
|
||||
return u.UnittestOptions.FailFast
|
||||
}
|
||||
|
||||
// Color returns the color flag
|
||||
func (u *UnittestImpl) Color() bool {
|
||||
return u.UnittestOptions.Color
|
||||
}
|
||||
|
||||
// DebugPlugin returns the debug plugin flag
|
||||
func (u *UnittestImpl) DebugPlugin() bool {
|
||||
return u.UnittestOptions.DebugPlugin
|
||||
}
|
||||
|
||||
// SkipCleanup returns the skip clean up
|
||||
func (u *UnittestImpl) SkipCleanup() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IncludeNeeds returns the include needs
|
||||
func (u *UnittestImpl) IncludeNeeds() bool {
|
||||
return u.UnittestOptions.IncludeNeeds || u.IncludeTransitiveNeeds()
|
||||
}
|
||||
|
||||
// IncludeTransitiveNeeds returns the include transitive needs
|
||||
func (u *UnittestImpl) IncludeTransitiveNeeds() bool {
|
||||
return u.UnittestOptions.IncludeTransitiveNeeds
|
||||
}
|
||||
|
||||
// SkipNeeds returns the skip needs
|
||||
func (u *UnittestImpl) SkipNeeds() bool {
|
||||
if !u.IncludeNeeds() {
|
||||
return u.UnittestOptions.SkipNeeds
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// EnforceNeedsAreInstalled returns false for unittest
|
||||
func (u *UnittestImpl) EnforceNeedsAreInstalled() bool {
|
||||
return false
|
||||
}
|
||||
|
|
@ -37,6 +37,7 @@ type Helm struct {
|
|||
Releases []Release
|
||||
Deleted []Release
|
||||
Linted []Release
|
||||
Unittested []Release
|
||||
Templated []Release
|
||||
Lists map[ListKey]string
|
||||
Diffs map[DiffKey]error
|
||||
|
|
@ -210,6 +211,13 @@ func (helm *Helm) TestRelease(context helmexec.HelmContext, name string, flags .
|
|||
func (helm *Helm) Fetch(chart string, flags ...string) error {
|
||||
return nil
|
||||
}
|
||||
func (helm *Helm) Unittest(name, chart string, flags ...string) error {
|
||||
if strings.Contains(name, "error") {
|
||||
return errors.New("error")
|
||||
}
|
||||
helm.Unittested = append(helm.Unittested, Release{Name: name, Flags: flags})
|
||||
return nil
|
||||
}
|
||||
func (helm *Helm) Lint(name, chart string, flags ...string) error {
|
||||
if strings.Contains(name, "error") {
|
||||
return errors.New("error")
|
||||
|
|
|
|||
|
|
@ -51,6 +51,8 @@ type execer struct {
|
|||
decryptedSecretMutex sync.Mutex
|
||||
decryptedSecrets map[string]*decryptedSecret
|
||||
writeTempFile func([]byte) (string, error)
|
||||
unittestPluginOnce sync.Once
|
||||
unittestPluginErr error
|
||||
}
|
||||
|
||||
func NewLogger(writer io.Writer, logLevel string) *zap.SugaredLogger {
|
||||
|
|
@ -744,6 +746,30 @@ func (helm *execer) Lint(name, chart string, flags ...string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func (helm *execer) Unittest(name, chart string, flags ...string) error {
|
||||
// Check if the helm-unittest plugin is installed (cached across invocations)
|
||||
helm.unittestPluginOnce.Do(func() {
|
||||
var pluginsDir string
|
||||
if helm.IsHelm3() {
|
||||
pluginsDir = cliv3.New().PluginsDirectory
|
||||
} else {
|
||||
pluginsDir = cliv4.New().PluginsDirectory
|
||||
}
|
||||
_, err := GetPluginVersion("unittest", pluginsDir)
|
||||
if err != nil {
|
||||
helm.unittestPluginErr = fmt.Errorf("helm-unittest plugin is required for `helmfile unittest`. Install it with: helm plugin install https://github.com/helm-unittest/helm-unittest: %w", err)
|
||||
}
|
||||
})
|
||||
if helm.unittestPluginErr != nil {
|
||||
return helm.unittestPluginErr
|
||||
}
|
||||
|
||||
helm.logger.Infof("Unit testing release=%v, chart=%v", name, chart)
|
||||
out, err := helm.exec(append([]string{"unittest", chart}, flags...), map[string]string{}, nil)
|
||||
helm.write(nil, out)
|
||||
return err
|
||||
}
|
||||
|
||||
func (helm *execer) Fetch(chart string, flags ...string) error {
|
||||
helm.logger.Infof("Fetching %v", redactedURL(chart))
|
||||
out, err := helm.exec(append([]string{"fetch", chart}, flags...), map[string]string{}, nil)
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ type Interface interface {
|
|||
ChartPull(chart string, path string, flags ...string) error
|
||||
ChartExport(chart string, path string) error
|
||||
Lint(name, chart string, flags ...string) error
|
||||
Unittest(name, chart string, flags ...string) error
|
||||
ReleaseStatus(context HelmContext, name string, flags ...string) error
|
||||
DeleteRelease(context HelmContext, name string, flags ...string) error
|
||||
TestRelease(context HelmContext, name string, flags ...string) error
|
||||
|
|
|
|||
|
|
@ -335,6 +335,10 @@ type ReleaseSpec struct {
|
|||
// Hooks is a list of extension points paired with operations, that are executed in specific points of the lifecycle of releases defined in helmfile
|
||||
Hooks []event.Hook `yaml:"hooks,omitempty"`
|
||||
|
||||
// UnitTests is a list of test file or directory paths for helm-unittest integration.
|
||||
// When specified, `helmfile unittest` will run `helm unittest` with the merged values and these test paths.
|
||||
UnitTests []string `yaml:"unitTests,omitempty"`
|
||||
|
||||
// Name is the name of this release
|
||||
Name string `yaml:"name,omitempty"`
|
||||
Namespace string `yaml:"namespace,omitempty"`
|
||||
|
|
@ -2180,6 +2184,157 @@ func (st *HelmState) LintReleases(helm helmexec.Interface, additionalValues []st
|
|||
return nil
|
||||
}
|
||||
|
||||
// UnittestOpts is the options for the unittest command
|
||||
type UnittestOpts struct {
|
||||
Set []string
|
||||
SkipCleanup bool
|
||||
FailFast bool
|
||||
Color bool
|
||||
DebugPlugin bool
|
||||
}
|
||||
|
||||
// UnittestOpt is a functional option for UnittestOpts
|
||||
type UnittestOpt interface {
|
||||
Apply(*UnittestOpts)
|
||||
}
|
||||
|
||||
// Apply implements UnittestOpt
|
||||
func (o *UnittestOpts) Apply(opts *UnittestOpts) {
|
||||
*opts = *o
|
||||
}
|
||||
|
||||
// UnittestReleases runs helm unittest on each release that has unitTests defined.
|
||||
// The workerLimit parameter is currently unused but kept for API consistency with
|
||||
// similar methods (e.g., LintReleases).
|
||||
func (st *HelmState) UnittestReleases(helm helmexec.Interface, additionalValues []string, args []string, _ int, opt ...UnittestOpt) []error {
|
||||
opts := &UnittestOpts{}
|
||||
for _, o := range opt {
|
||||
o.Apply(opts)
|
||||
}
|
||||
|
||||
helm.SetExtraArgs()
|
||||
|
||||
errs := []error{}
|
||||
|
||||
if len(args) > 0 {
|
||||
helm.SetExtraArgs(args...)
|
||||
}
|
||||
|
||||
for i := range st.Releases {
|
||||
release := st.Releases[i]
|
||||
|
||||
if !release.Desired() {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(release.UnitTests) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
flags, files, err := st.flagsForLint(helm, &release, 0)
|
||||
|
||||
if !opts.SkipCleanup {
|
||||
defer st.removeFiles(files)
|
||||
}
|
||||
|
||||
releaseErr := false
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
releaseErr = true
|
||||
}
|
||||
for _, value := range additionalValues {
|
||||
valfile, err := filepath.Abs(value)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
releaseErr = true
|
||||
break
|
||||
}
|
||||
|
||||
// Check for any stat error (not just IsNotExist) to also catch
|
||||
// permission denied, I/O errors, etc. before passing to helm.
|
||||
// This intentionally differs from LintReleases which only checks IsNotExist.
|
||||
if _, err := os.Stat(valfile); err != nil {
|
||||
errs = append(errs, err)
|
||||
releaseErr = true
|
||||
break
|
||||
}
|
||||
flags = append(flags, "--values", valfile)
|
||||
}
|
||||
|
||||
if releaseErr {
|
||||
continue
|
||||
}
|
||||
|
||||
if opts.Set != nil {
|
||||
for _, s := range opts.Set {
|
||||
flags = append(flags, "--set", s)
|
||||
}
|
||||
}
|
||||
|
||||
if opts.FailFast {
|
||||
flags = append(flags, "--failfast")
|
||||
}
|
||||
|
||||
if opts.Color {
|
||||
// In Helm 4, --color is parsed by Helm itself before reaching the plugin.
|
||||
// See https://github.com/helmfile/helmfile/issues/2280 for details.
|
||||
// Skip the flag with a warning since helm-unittest does not currently
|
||||
// support an env var alternative for colored output.
|
||||
if helm.IsHelm4() {
|
||||
st.logger.Warnf("warn: --color flag is not supported with Helm 4 due to flag parsing issues, ignoring\n")
|
||||
} else {
|
||||
flags = append(flags, "--color")
|
||||
}
|
||||
}
|
||||
|
||||
if opts.DebugPlugin {
|
||||
flags = append(flags, "--debugPlugin")
|
||||
}
|
||||
|
||||
// Add unit test file/directory paths as glob patterns for --file flag.
|
||||
// Paths are relative to the chart directory (matching helm-unittest conventions).
|
||||
// If the path has no glob characters and does not look like a YAML file,
|
||||
// treat it as a directory and append a glob suffix.
|
||||
// Validate and add unit test file/directory paths.
|
||||
// Reject absolute paths and paths that escape the chart directory via "..".
|
||||
for _, testPath := range release.UnitTests {
|
||||
cleanPath := filepath.Clean(testPath)
|
||||
if filepath.IsAbs(cleanPath) || cleanPath == ".." || strings.HasPrefix(cleanPath, ".."+string(filepath.Separator)) {
|
||||
errs = append(errs, fmt.Errorf("release %q: unitTests path %q must be a relative path within the chart directory", release.Name, testPath))
|
||||
releaseErr = true
|
||||
break
|
||||
}
|
||||
if !strings.ContainsAny(testPath, "*?[") {
|
||||
lowerPath := strings.ToLower(testPath)
|
||||
if !strings.HasSuffix(lowerPath, ".yaml") && !strings.HasSuffix(lowerPath, ".yml") {
|
||||
testPath = strings.TrimRight(testPath, "/") + "/*_test.yaml"
|
||||
}
|
||||
}
|
||||
flags = append(flags, "--file", testPath)
|
||||
}
|
||||
|
||||
if len(errs) == 0 || !opts.FailFast {
|
||||
if err := helm.Unittest(release.Name, release.ChartPathOrName(), flags...); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := st.TriggerCleanupEvent(&release, "unittest"); err != nil {
|
||||
st.logger.Warnf("warn: %v\n", err)
|
||||
}
|
||||
|
||||
if opts.FailFast && len(errs) > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) != 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type diffResult struct {
|
||||
release *ReleaseSpec
|
||||
err *ReleaseError
|
||||
|
|
|
|||
|
|
@ -38,39 +38,39 @@ func TestGenerateID(t *testing.T) {
|
|||
run(testcase{
|
||||
subject: "baseline",
|
||||
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
|
||||
want: "foo-values-66f7fd6f7b",
|
||||
want: "foo-values-6884949b8b",
|
||||
})
|
||||
|
||||
run(testcase{
|
||||
subject: "different bytes content",
|
||||
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
|
||||
data: []byte(`{"k":"v"}`),
|
||||
want: "foo-values-6664979cd7",
|
||||
want: "foo-values-58f57b794f",
|
||||
})
|
||||
|
||||
run(testcase{
|
||||
subject: "different map content",
|
||||
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
|
||||
data: map[string]any{"k": "v"},
|
||||
want: "foo-values-78897dfd49",
|
||||
want: "foo-values-6b6b884cc9",
|
||||
})
|
||||
|
||||
run(testcase{
|
||||
subject: "different chart",
|
||||
release: ReleaseSpec{Name: "foo", Chart: "stable/envoy"},
|
||||
want: "foo-values-64b7846cb7",
|
||||
want: "foo-values-85494c4677",
|
||||
})
|
||||
|
||||
run(testcase{
|
||||
subject: "different name",
|
||||
release: ReleaseSpec{Name: "bar", Chart: "incubator/raw"},
|
||||
want: "bar-values-576cb7ddc7",
|
||||
want: "bar-values-9d65c65f",
|
||||
})
|
||||
|
||||
run(testcase{
|
||||
subject: "specific ns",
|
||||
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw", Namespace: "myns"},
|
||||
want: "myns-foo-values-6c567f54c",
|
||||
want: "myns-foo-values-84b69bb989",
|
||||
})
|
||||
|
||||
for id, n := range ids {
|
||||
|
|
|
|||
|
|
@ -142,6 +142,10 @@ func (helm *noCallHelmExec) Lint(name, chart string, flags ...string) error {
|
|||
helm.doPanic()
|
||||
return nil
|
||||
}
|
||||
func (helm *noCallHelmExec) Unittest(name, chart string, flags ...string) error {
|
||||
helm.doPanic()
|
||||
return nil
|
||||
}
|
||||
func (helm *noCallHelmExec) IsHelm3() bool {
|
||||
helm.doPanic()
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -129,6 +129,7 @@ ${kubectl} create namespace ${test_ns} || fail "Could not create namespace ${tes
|
|||
. ${dir}/test-cases/issue-2309-kube-context-template.sh
|
||||
. ${dir}/test-cases/issue-2355.sh
|
||||
. ${dir}/test-cases/issue-2103.sh
|
||||
. ${dir}/test-cases/unittest.sh
|
||||
|
||||
# ALL DONE -----------------------------------------------------------------------------------------------------------
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
unittest_input_dir="${cases_dir}/unittest/input"
|
||||
helmfile_real="$(pwd)/${helmfile}"
|
||||
HELM_UNITTEST_VERSION="${HELM_UNITTEST_VERSION:-1.0.3}"
|
||||
|
||||
# Ensure helm-unittest plugin is installed (matching plugin install pattern from run.sh)
|
||||
info "Ensuring helm-unittest plugin v${HELM_UNITTEST_VERSION} is installed"
|
||||
${helm} plugin ls | grep "^unittest" || ${helm} plugin install https://github.com/helm-unittest/helm-unittest --version v${HELM_UNITTEST_VERSION} ${PLUGIN_INSTALL_FLAGS} || fail "Could not install helm-unittest plugin"
|
||||
|
||||
test_start "helmfile unittest - runs unit tests on releases with unitTests defined"
|
||||
cd "${unittest_input_dir}"
|
||||
${helmfile_real} unittest || fail "helmfile unittest should succeed"
|
||||
cd -
|
||||
test_pass "helmfile unittest - runs unit tests on releases with unitTests defined"
|
||||
|
||||
test_start "helmfile unittest - with selector targeting release without unitTests"
|
||||
cd "${unittest_input_dir}"
|
||||
${helmfile_real} -l name=no-tests-app unittest || fail "helmfile unittest should succeed for releases without unitTests (skips them)"
|
||||
cd -
|
||||
test_pass "helmfile unittest - with selector targeting release without unitTests"
|
||||
|
||||
test_start "helmfile unittest - with selector targeting release with unitTests"
|
||||
cd "${unittest_input_dir}"
|
||||
${helmfile_real} -l name=test-app unittest || fail "helmfile unittest should succeed for releases with unitTests"
|
||||
cd -
|
||||
test_pass "helmfile unittest - with selector targeting release with unitTests"
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
apiVersion: v2
|
||||
name: test-app
|
||||
description: A test chart for helmfile unittest integration test
|
||||
type: application
|
||||
version: 0.1.0
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ .Release.Name }}
|
||||
labels:
|
||||
app: {{ .Release.Name }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: {{ .Release.Name }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: {{ .Release.Name }}
|
||||
spec:
|
||||
containers:
|
||||
- name: {{ .Release.Name }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
ports:
|
||||
- containerPort: {{ .Values.service.port }}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ .Release.Name }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
targetPort: {{ .Values.service.port }}
|
||||
selector:
|
||||
app: {{ .Release.Name }}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
suite: test deployment
|
||||
templates:
|
||||
- templates/deployment.yaml
|
||||
tests:
|
||||
- it: should create deployment with correct replicas
|
||||
set:
|
||||
replicaCount: 3
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.replicas
|
||||
value: 3
|
||||
|
||||
- it: should use the correct image
|
||||
set:
|
||||
image.repository: nginx
|
||||
image.tag: "1.21"
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.template.spec.containers[0].image
|
||||
value: "nginx:1.21"
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
replicaCount: 1
|
||||
|
||||
image:
|
||||
repository: nginx
|
||||
tag: latest
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 80
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
releases:
|
||||
- name: test-app
|
||||
chart: ./charts/test-app
|
||||
values:
|
||||
- ./charts/test-app/values.yaml
|
||||
unitTests:
|
||||
- tests
|
||||
|
||||
- name: no-tests-app
|
||||
chart: ./charts/test-app
|
||||
Loading…
Reference in New Issue