add helm-unittest integration
Signed-off-by: yxxhero <aiopsclub@163.com>
This commit is contained in:
parent
400a67fd88
commit
0ecbe8429b
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 != "" {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue