From 811654b672b91b1c5a608c714f129f8de855cb72 Mon Sep 17 00:00:00 2001 From: Hemant Pandey Date: Sat, 18 Nov 2023 20:52:01 +0000 Subject: [PATCH] feat: Add support for running helm tests for a release --- client.go | 41 +++++++++++++++++++++++++++++++++++++++++ go.mod | 3 ++- go.sum | 8 +++++--- interface.go | 1 + mock/interface.go | 15 +++++++++++++++ 5 files changed, 64 insertions(+), 4 deletions(-) diff --git a/client.go b/client.go index 2c65d157..3a23113d 100644 --- a/client.go +++ b/client.go @@ -10,6 +10,8 @@ import ( "reflect" "strings" + "golang.org/x/exp/slices" + "k8s.io/apimachinery/pkg/api/errors" "github.com/spf13/pflag" @@ -773,6 +775,28 @@ func (c *HelmClient) GetChart(chartName string, chartPathOptions *action.ChartPa return helmChart, chartPath, err } +// RunTests runs the tests that were deployed with the release provided. It returns true +// if all the tests ran successfully and false in all other cases. +// NOTE: error = nil implies that all tests ran to either success or failure. +func (c *HelmClient) RunChartTests(releaseName string) (bool, error) { + + client := action.NewReleaseTesting(c.ActionConfig) + + if c.Settings.Namespace() == "" { + return false, fmt.Errorf("namespace not set") + } + + client.Namespace = c.Settings.Namespace() + + rel, err := client.Run(releaseName) + if err != nil && rel == nil { + return false, fmt.Errorf("unable to find release '%s': %v", releaseName, err) + } + + // Check that there are no test failures + return checkReleaseForTestFailure(rel) == false, nil +} + // chartExists checks whether a chart is already installed // in a namespace or not based on the provided chart spec. // Note that this function only considers the contained chart name and namespace. @@ -856,6 +880,23 @@ func updateDependencies(helmChart *chart.Chart, chartPathOptions *action.ChartPa return helmChart, nil } +// checkReleaseForTestFailure parses the list of hooks in the release +// and checks the status of the test hooks, returning true if any test has Phase != Succeeded +// Returns false if all tests have passed (including if there are no tests) +func checkReleaseForTestFailure(rel *release.Release) bool { + // Check if any test failed + hooksToCheck := []*release.Hook{} + for _, hook := range rel.Hooks { + // Only check the Phase for events which are supposed to get triggered for "test" hook + if slices.Contains(hook.Events, release.HookTest) { + hooksToCheck = append(hooksToCheck, hook) + } + } + return slices.ContainsFunc(hooksToCheck, func(h *release.Hook) bool { + return h.LastRun.Phase != release.HookPhaseSucceeded + }) +} + // mergeRollbackOptions merges values of the provided chart to helm rollback options used by the client. func mergeRollbackOptions(chartSpec *ChartSpec, rollbackOptions *action.Rollback) { rollbackOptions.DisableHooks = chartSpec.DisableHooks diff --git a/go.mod b/go.mod index 10c575b5..778f32a5 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/golang/mock v1.6.0 github.com/pkg/errors v0.9.1 github.com/spf13/pflag v1.0.5 + golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa helm.sh/helm/v3 v3.11.2 k8s.io/apiextensions-apiserver v0.26.3 k8s.io/apimachinery v0.27.1 @@ -108,7 +109,7 @@ require ( golang.org/x/net v0.8.0 // indirect golang.org/x/oauth2 v0.1.0 // indirect golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.6.0 // indirect + golang.org/x/sys v0.14.0 // indirect golang.org/x/term v0.6.0 // indirect golang.org/x/text v0.8.0 // indirect golang.org/x/time v0.1.0 // indirect diff --git a/go.sum b/go.sum index 28c6564d..b1c6ae36 100644 --- a/go.sum +++ b/go.sum @@ -663,6 +663,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= +golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -836,8 +838,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20221013171732-95e765b1cc43/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -919,7 +921,7 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= +golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/interface.go b/interface.go index dd1f4ba3..eb716d29 100644 --- a/interface.go +++ b/interface.go @@ -30,6 +30,7 @@ type Client interface { SetDebugLog(debugLog action.DebugLog) ListReleaseHistory(name string, max int) ([]*release.Release, error) GetChart(chartName string, chartPathOptions *action.ChartPathOptions) (*chart.Chart, string, error) + RunChartTests(releaseName string) (bool, error) } type RollBack interface { diff --git a/mock/interface.go b/mock/interface.go index 0fdc07a9..7657339b 100644 --- a/mock/interface.go +++ b/mock/interface.go @@ -202,6 +202,21 @@ func (mr *MockClientMockRecorder) RollbackRelease(spec interface{}) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RollbackRelease", reflect.TypeOf((*MockClient)(nil).RollbackRelease), spec) } +// RunChartTests mocks base method. +func (m *MockClient) RunChartTests(releaseName string) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RunChartTests", releaseName) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RunChartTests indicates an expected call of RunChartTests. +func (mr *MockClientMockRecorder) RunChartTests(releaseName interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RunChartTests", reflect.TypeOf((*MockClient)(nil).RunChartTests), releaseName) +} + // SetDebugLog mocks base method. func (m *MockClient) SetDebugLog(debugLog action.DebugLog) { m.ctrl.T.Helper()