add helm-unittest integration

Signed-off-by: yxxhero <aiopsclub@163.com>
This commit is contained in:
yxxhero 2024-04-12 08:36:18 +08:00
parent 400a67fd88
commit 0ecbe8429b
10 changed files with 382 additions and 0 deletions

View File

@ -1584,6 +1584,29 @@ Do you really want to delete?
return true, errs
}
func (a *App) unittest(r *Run, c UnittestConfigProvider) (bool, []error) {
ok, errs := a.withNeeds(r, c, true, func(st *state.HelmState) []error {
helm := r.helm
opts := &state.UnittestOpts{
Color: c.Color(),
DebugPlugin: c.DebugPlugin(),
FailFast: c.FailFast(),
UnittestArgs: c.UnittestArgs(),
}
filtered := &Run{
state: st,
helm: helm,
ctx: r.ctx,
Ask: r.Ask,
}
return filtered.unittest(true, c, opts)
})
return ok, errs
}
func (a *App) diff(r *Run, c DiffConfigProvider) (*string, bool, bool, []error) {
var (
infoMsg *string

View File

@ -2510,6 +2510,10 @@ func (helm *mockHelmExec) SyncRelease(context helmexec.HelmContext, name, chart
func (helm *mockHelmExec) DiffRelease(context helmexec.HelmContext, name, chart string, suppressDiff bool, flags ...string) error {
return nil
}
func (helm *mockHelmExec) UnittestRelease(context helmexec.HelmContext, name, chart string, flags ...string) error {
return nil
}
func (helm *mockHelmExec) ReleaseStatus(context helmexec.HelmContext, release string, flags ...string) error {
return nil
}

View File

@ -121,6 +121,15 @@ type SyncConfigProvider interface {
valuesControlMode
}
type UnittestConfigProvider interface {
Color() bool
DebugPlugin() bool
Values() []string
FailFast() bool
UnittestArgs() []string
concurrencyConfig
DAGConfig
}
type DiffConfigProvider interface {
Args() string
PostRenderer() string

View File

@ -23,6 +23,7 @@ const (
HelmSecretsRecommendedVersion = "v4.6.0"
HelmGitRecommendedVersion = "v0.15.1"
HelmS3RecommendedVersion = "v0.16.0"
HelmUninttestVersion = "v0.4.4"
HelmInstallCommand = "https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3"
)
@ -53,6 +54,11 @@ var (
version: HelmGitRecommendedVersion,
repo: "https://github.com/aslafy-z/helm-git.git",
},
{
name: "helm-unittest",
version: HelmUninttestVersion,
repo: "https://github.com/helm-unittest/helm-unittest.git",
},
}
)

View File

@ -213,3 +213,29 @@ func (r *Run) diff(triggerCleanupEvent bool, detailedExitCode bool, c DiffConfig
return &infoMsg, releasesToBeUpdated, releasesToBeDeleted, nil
}
func (r *Run) unittest(triggerCleanupEvent bool, c UnittestConfigProvider, unittestOpts *state.UnittestOpts) []error {
st := r.state
helm := r.helm
planningErrs := st.UninttestReleases(helm, c.Values(), c.Concurrency(), triggerCleanupEvent, unittestOpts)
fatalErrs := []error{}
for _, e := range planningErrs {
switch err := e.(type) {
case *state.ReleaseError:
if err.Code != 2 {
fatalErrs = append(fatalErrs, e)
}
default:
fatalErrs = append(fatalErrs, e)
}
}
if len(fatalErrs) > 0 {
return fatalErrs
}
return nil
}

View File

@ -117,6 +117,10 @@ func (helm *Helm) SyncRelease(context helmexec.HelmContext, name, chart string,
return nil
}
func (helm *Helm) UnittestRelease(context helmexec.HelmContext, name, chart string, flags ...string) error {
return nil
}
func (helm *Helm) DiffRelease(context helmexec.HelmContext, name, chart string, suppressDiff bool, flags ...string) error {
if helm.DiffMutex != nil {
helm.DiffMutex.Lock()

View File

@ -434,6 +434,23 @@ func (helm *execer) TemplateRelease(name string, chart string, flags ...string)
return err
}
func (helm *execer) UnittestRelease(context HelmContext, name, chart string, flags ...string) error {
if context.Writer != nil {
fmt.Fprintf(context.Writer, "Unittestting release=%v, chart=%v\n", name, redactedURL(chart))
} else {
helm.logger.Infof("Comparing 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)
// Do our best to write STDOUT only when diff existed
// Unfortunately, this works only when you run helmfile with `--detailed-exitcode`
helm.write(context.Writer, out)
return err
}
func (helm *execer) DiffRelease(context HelmContext, name, chart string, suppressDiff bool, flags ...string) error {
if context.Writer != nil {
fmt.Fprintf(context.Writer, "Comparing release=%v, chart=%v\n", name, redactedURL(chart))

View File

@ -23,6 +23,7 @@ type Interface interface {
UpdateDeps(chart string) error
SyncRelease(context HelmContext, name, chart string, flags ...string) error
DiffRelease(context HelmContext, name, chart string, suppressDiff bool, flags ...string) error
UnittestRelease(context HelmContext, name, chart string, flags ...string) error
TemplateRelease(name, chart string, flags ...string) error
Fetch(chart string, flags ...string) error
ChartPull(chart string, path string, flags ...string) error

View File

@ -387,6 +387,9 @@ type ReleaseSpec struct {
DeleteWait *bool `yaml:"deleteWait,omitempty"`
// Timeout is the time in seconds to wait for helmfile delete command (default 300)
DeleteTimeout *int `yaml:"deleteTimeout,omitempty"`
// https://github.com/helmfile/helmfile/discussions/1445, add integration with helm-unittest
UnitTests []string `yaml:"unitTests,omitempty"`
}
func (r *Inherits) UnmarshalYAML(unmarshal func(any) error) error {
@ -1727,6 +1730,19 @@ type diffPrepareResult struct {
suppressDiff bool
}
type unittestResult struct {
release *ReleaseSpec
err *ReleaseError
buf *bytes.Buffer
}
type unittestPrepareResult struct {
release *ReleaseSpec
flags []string
errors []*ReleaseError
files []string
}
// commonDiffFlags returns common flags for helm diff, not in release-specific context
func (st *HelmState) commonDiffFlags(detailedExitCode bool, stripTrailingCR bool, includeTests bool, suppress []string, suppressSecrets bool, showSecrets bool, noHooks bool, opt *DiffOpts) []string {
var flags []string
@ -1785,6 +1801,123 @@ func (st *HelmState) commonDiffFlags(detailedExitCode bool, stripTrailingCR bool
return flags
}
func (st *HelmState) commonUnittestFlags(opt *UnittestOpts) []string {
var flags []string
if opt.Color {
flags = append(flags, "--color")
}
if opt.DebugPlugin {
flags = append(flags, "--debug-plugin")
}
if opt.FailFast {
flags = append(flags, "--fail-fast")
}
return flags
}
func (st *HelmState) prepareUnittestReleases(helm helmexec.Interface, additionalValues []string, concurrency int, opts ...UnittestOpt) ([]unittestPrepareResult, []error) {
opt := &UnittestOpts{}
for _, o := range opts {
o.Apply(opt)
}
releases := []*ReleaseSpec{}
for i := range st.Releases {
if !st.Releases[i].Desired() {
continue
}
if st.Releases[i].Installed != nil && !*(st.Releases[i].Installed) {
continue
}
releases = append(releases, &st.Releases[i])
}
numReleases := len(releases)
jobs := make(chan *ReleaseSpec, numReleases)
results := make(chan unittestPrepareResult, numReleases)
resultsMap := map[string]unittestPrepareResult{}
commonUnittestFlags := st.commonUnittestFlags(opt)
rs := []unittestPrepareResult{}
errs := []error{}
mut := sync.Mutex{}
st.scatterGather(
concurrency,
numReleases,
func() {
for i := 0; i < numReleases; i++ {
jobs <- releases[i]
}
close(jobs)
},
func(workerIndex int) {
for release := range jobs {
errs := []error{}
st.ApplyOverrides(release)
mut.Lock()
flags, files, err := st.flagsForUnittest(helm, release, workerIndex)
mut.Unlock()
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, commonUnittestFlags...)
if len(errs) > 0 {
rsErrs := make([]*ReleaseError, len(errs))
for i, e := range errs {
rsErrs[i] = newReleaseFailedError(release, e)
}
results <- unittestPrepareResult{errors: rsErrs, flags: files}
} else {
results <- unittestPrepareResult{release: release, flags: flags, files: files, errors: []*ReleaseError{}}
}
}
},
func() {
for i := 0; i < numReleases; i++ {
res := <-results
if res.errors != nil && len(res.errors) > 0 {
for _, e := range res.errors {
errs = append(errs, e)
}
} else if res.release != nil {
resultsMap[ReleaseToID(res.release)] = res
}
}
},
)
for _, r := range releases {
if p, ok := resultsMap[ReleaseToID(r)]; ok {
rs = append(rs, p)
}
}
return rs, errs
}
func (st *HelmState) prepareDiffReleases(helm helmexec.Interface, additionalValues []string, concurrency int, detailedExitCode bool, stripTrailingCR bool, includeTests bool, suppress []string, suppressSecrets bool, showSecrets bool, noHooks bool, opts ...DiffOpt) ([]diffPrepareResult, []error) {
opt := &DiffOpts{}
for _, o := range opts {
@ -1947,6 +2080,102 @@ func (st *HelmState) createHelmContextWithWriter(spec *ReleaseSpec, w io.Writer)
return ctx
}
type UnittestOpts struct {
Color bool
DebugPlugin bool
FailFast bool
UnittestArgs []string
}
func (o *UnittestOpts) Apply(opts *UnittestOpts) {
*opts = *o
}
type UnittestOpt interface{ Apply(*UnittestOpts) }
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)
}
preps, prepErrs := st.prepareUnittestReleases(helm, additionalValues, workerLimit, opts)
defer func() {
for _, p := range preps {
st.removeFiles(p.files)
}
}()
if len(prepErrs) > 0 {
return prepErrs
}
jobQueue := make(chan *unittestPrepareResult, len(preps))
results := make(chan unittestResult, len(preps))
outputs := map[string]*bytes.Buffer{}
errs := []error{}
st.scatterGather(
workerLimit,
len(preps),
func() {
for i := 0; i < len(preps); i++ {
jobQueue <- &preps[i]
}
close(jobQueue)
},
func(workerIndex int) {
for prep := range jobQueue {
flags := prep.flags
release := prep.release
buf := &bytes.Buffer{}
if err := helm.UnittestRelease(st.createHelmContextWithWriter(release, buf), release.Name, normalizeChart(st.basePath, release.ChartPathOrName()), flags...); err != nil {
switch e := err.(type) {
case helmexec.ExitError:
// Propagate any non-zero exit status from the external command like `helm` that is failed under the hood
results <- unittestResult{release, &ReleaseError{release, err, e.ExitStatus()}, buf}
default:
results <- unittestResult{release, &ReleaseError{release, err, 0}, buf}
}
} else {
// diff succeeded, found no changes
results <- unittestResult{release, nil, buf}
}
if triggerCleanupEvents {
if _, err := st.TriggerCleanupEvent(prep.release, "diff"); err != nil {
st.logger.Warnf("warn: %v\n", err)
}
}
}
},
func() {
for i := 0; i < len(preps); i++ {
res := <-results
if res.err != nil {
errs = append(errs, res.err)
}
outputs[ReleaseToID(res.release)] = res.buf
}
},
)
for _, p := range preps {
id := ReleaseToID(p.release)
if stdout, ok := outputs[id]; ok {
fmt.Print(stdout.String())
} else {
panic(fmt.Sprintf("missing output for release %s", id))
}
}
return errs
}
type DiffOpts struct {
Context int
Output string
@ -2708,6 +2937,10 @@ func (st *HelmState) flagsForTemplate(helm helmexec.Interface, release *ReleaseS
return append(flags, common...), files, nil
}
func (st *HelmState) flagsForUnittest(helm helmexec.Interface, release *ReleaseSpec, workerIndex int) ([]string, []string, error) {
return st.valuesFlags(helm, release, workerIndex)
}
func (st *HelmState) flagsForDiff(helm helmexec.Interface, release *ReleaseSpec, disableValidation bool, workerIndex int, opt *DiffOpts) ([]string, []string, error) {
settings := cli.New()
flags := st.chartVersionFlags(release)
@ -3157,6 +3390,60 @@ func (st *HelmState) generateValuesFiles(helm helmexec.Interface, release *Relea
return files, nil
}
func (st *HelmState) valuesFlags(helm helmexec.Interface, release *ReleaseSpec, workerIndex int) ([]string, []string, error) {
flags := []string{}
var files []string
generatedFiles, err := st.generateValuesFiles(helm, release, workerIndex)
if err != nil {
return nil, files, err
}
files = generatedFiles
for _, f := range generatedFiles {
flags = append(flags, "--values", f)
}
if len(release.SetValues) > 0 {
setFlags, err := st.setFlags(release.SetValues)
if err != nil {
return nil, files, fmt.Errorf("Failed to render set value entry in %s for release %s: %v", st.FilePath, release.Name, err)
}
flags = append(flags, setFlags...)
}
/***********
* START 'env' section for backwards compatibility
***********/
// The 'env' section is not really necessary any longer, as 'set' would now provide the same functionality
if len(release.EnvValues) > 0 {
val := []string{}
envValErrs := []string{}
for _, set := range release.EnvValues {
value, isSet := os.LookupEnv(set.Value)
if isSet {
val = append(val, fmt.Sprintf("%s=%s", escape(set.Name), escape(value)))
} else {
errMsg := fmt.Sprintf("\t%s", set.Value)
envValErrs = append(envValErrs, errMsg)
}
}
if len(envValErrs) != 0 {
joinedEnvVals := strings.Join(envValErrs, "\n")
errMsg := fmt.Sprintf("Environment Variables not found. Please make sure they are set and try again:\n%s", joinedEnvVals)
return nil, files, errors.New(errMsg)
}
flags = append(flags, "--set", strings.Join(val, ","))
}
/**************
* END 'env' section for backwards compatibility
**************/
return flags, files, nil
}
func (st *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, release *ReleaseSpec, workerIndex int) ([]string, []string, error) {
flags := []string{}
if release.Namespace != "" {

View File

@ -97,6 +97,11 @@ func (helm *noCallHelmExec) DiffRelease(context helmexec.HelmContext, name, char
helm.doPanic()
return nil
}
func (helm *noCallHelmExec) UnittestRelease(context helmexec.HelmContext, name, chart string, flags ...string) error {
helm.doPanic()
return nil
}
func (helm *noCallHelmExec) ReleaseStatus(context helmexec.HelmContext, release string, flags ...string) error {
helm.doPanic()
return nil