Merge pull request #6054 from jlegrone/test-as-hook

Support defining tests as Job resources
pull/6208/head
Matthew Fisher 5 years ago committed by GitHub
commit c6d6e456d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -25,7 +25,7 @@ func TestInstall(t *testing.T) {
// Install, base case
{
name: "basic install",
cmd: "install aeneas testdata/testcharts/empty",
cmd: "install aeneas testdata/testcharts/empty --namespace default",
golden: "output/install.txt",
},

@ -16,16 +16,13 @@ limitations under the License.
package main
import (
"fmt"
"io"
"time"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"helm.sh/helm/cmd/helm/require"
"helm.sh/helm/pkg/action"
"helm.sh/helm/pkg/release"
)
const releaseTestRunHelp = `
@ -44,27 +41,7 @@ func newReleaseTestRunCmd(cfg *action.Configuration, out io.Writer) *cobra.Comma
Long: releaseTestRunHelp,
Args: require.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
c, errc := client.Run(args[0])
testErr := &testErr{}
for {
select {
case err := <-errc:
if err != nil && testErr.failed > 0 {
return testErr.Error()
}
return err
case res, ok := <-c:
if !ok {
break
}
if res.Status == release.TestRunFailure {
testErr.failed++
}
fmt.Fprintf(out, res.Msg+"\n")
}
}
return client.Run(args[0])
},
}
@ -74,11 +51,3 @@ func newReleaseTestRunCmd(cfg *action.Configuration, out io.Writer) *cobra.Comma
return cmd
}
type testErr struct {
failed int
}
func (err *testErr) Error() error {
return errors.Errorf("%v test(s) failed", err.failed)
}

@ -25,12 +25,14 @@ import (
)
func TestStatusCmd(t *testing.T) {
releasesMockWithStatus := func(info *release.Info) []*release.Release {
releasesMockWithStatus := func(info *release.Info, hooks ...*release.Hook) []*release.Release {
info.LastDeployed = time.Unix(1452902400, 0).UTC()
return []*release.Release{{
Name: "flummoxed-chickadee",
Info: info,
Chart: &chart.Chart{},
Name: "flummoxed-chickadee",
Namespace: "default",
Info: info,
Chart: &chart.Chart{},
Hooks: hooks,
}}
}
@ -77,19 +79,47 @@ func TestStatusCmd(t *testing.T) {
name: "get status of a deployed release with test suite",
cmd: "status flummoxed-chickadee",
golden: "output/status-with-test-suite.txt",
rels: releasesMockWithStatus(&release.Info{
Status: release.StatusDeployed,
LastTestSuiteRun: &release.TestSuite{
Results: []*release.TestRun{{
Name: "test run 1",
Status: release.TestRunSuccess,
Info: "extra info",
}, {
Name: "test run 2",
Status: release.TestRunFailure,
}},
rels: releasesMockWithStatus(
&release.Info{
Status: release.StatusDeployed,
},
}),
&release.Hook{
Name: "never-run-test",
Events: []release.HookEvent{release.HookTest},
},
&release.Hook{
Name: "passing-test",
Events: []release.HookEvent{release.HookTest},
LastRun: release.HookExecution{
StartedAt: mustParseTime("2006-01-02T15:04:05Z"),
CompletedAt: mustParseTime("2006-01-02T15:04:07Z"),
Phase: release.HookPhaseSucceeded,
},
},
&release.Hook{
Name: "failing-test",
Events: []release.HookEvent{release.HookTest},
LastRun: release.HookExecution{
StartedAt: mustParseTime("2006-01-02T15:10:05Z"),
CompletedAt: mustParseTime("2006-01-02T15:10:07Z"),
Phase: release.HookPhaseFailed,
},
},
&release.Hook{
Name: "passing-pre-install",
Events: []release.HookEvent{release.HookPreInstall},
LastRun: release.HookExecution{
StartedAt: mustParseTime("2006-01-02T15:00:05Z"),
CompletedAt: mustParseTime("2006-01-02T15:00:07Z"),
Phase: release.HookPhaseSucceeded,
},
},
),
}}
runTestCmd(t, tests)
}
func mustParseTime(t string) time.Time {
res, _ := time.Parse(time.RFC3339, t)
return res
}

@ -1,6 +1,6 @@
NAME: flummoxed-chickadee
LAST DEPLOYED: 2016-01-16 00:00:00 +0000 UTC
NAMESPACE:
NAMESPACE: default
STATUS: deployed
NOTES:

@ -1,6 +1,6 @@
NAME: flummoxed-chickadee
LAST DEPLOYED: 2016-01-16 00:00:00 +0000 UTC
NAMESPACE:
NAMESPACE: default
STATUS: deployed
RESOURCES:

@ -1,12 +1,15 @@
NAME: flummoxed-chickadee
LAST DEPLOYED: 2016-01-16 00:00:00 +0000 UTC
NAMESPACE:
NAMESPACE: default
STATUS: deployed
TEST SUITE:
Last Started: 0001-01-01 00:00:00 +0000 UTC
Last Completed: 0001-01-01 00:00:00 +0000 UTC
TEST SUITE: passing-test
Last Started: 2006-01-02 15:04:05 +0000 UTC
Last Completed: 2006-01-02 15:04:07 +0000 UTC
Phase: Succeeded
TEST SUITE: failing-test
Last Started: 2006-01-02 15:10:05 +0000 UTC
Last Completed: 2006-01-02 15:10:07 +0000 UTC
Phase: Failed
TEST STATUS INFO STARTED COMPLETED
test run 1 success extra info 0001-01-01 00:00:00 +0000 UTC 0001-01-01 00:00:00 +0000 UTC
test run 2 failure 0001-01-01 00:00:00 +0000 UTC 0001-01-01 00:00:00 +0000 UTC

@ -1 +1 @@
{"name":"flummoxed-chickadee","info":{"first_deployed":"0001-01-01T00:00:00Z","last_deployed":"2016-01-16T00:00:00Z","deleted":"0001-01-01T00:00:00Z","status":"deployed","notes":"release notes"}}
{"name":"flummoxed-chickadee","info":{"first_deployed":"0001-01-01T00:00:00Z","last_deployed":"2016-01-16T00:00:00Z","deleted":"0001-01-01T00:00:00Z","status":"deployed","notes":"release notes"},"namespace":"default"}

@ -1,5 +1,5 @@
NAME: flummoxed-chickadee
LAST DEPLOYED: 2016-01-16 00:00:00 +0000 UTC
NAMESPACE:
NAMESPACE: default
STATUS: deployed

@ -7,3 +7,4 @@ info:
resource B
status: deployed
name: flummoxed-chickadee
namespace: default

@ -17,11 +17,8 @@ limitations under the License.
package action
import (
"bytes"
"path"
"regexp"
"sort"
"strings"
"time"
"github.com/pkg/errors"
@ -32,7 +29,6 @@ import (
"helm.sh/helm/internal/experimental/registry"
"helm.sh/helm/pkg/chartutil"
"helm.sh/helm/pkg/hooks"
"helm.sh/helm/pkg/kube"
"helm.sh/helm/pkg/release"
"helm.sh/helm/pkg/storage"
@ -204,75 +200,3 @@ type RESTClientGetter interface {
ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error)
ToRESTMapper() (meta.RESTMapper, error)
}
// execHooks is a method for exec-ing all hooks of the given type. This is to
// avoid duplicate code in various actions
func execHooks(client kube.Interface, hs []*release.Hook, hook string, timeout time.Duration) error {
executingHooks := []*release.Hook{}
for _, h := range hs {
for _, e := range h.Events {
if string(e) == hook {
executingHooks = append(executingHooks, h)
}
}
}
sort.Sort(hookByWeight(executingHooks))
for _, h := range executingHooks {
if err := deleteHookByPolicy(client, h, hooks.BeforeHookCreation); err != nil {
return err
}
resources, err := client.Build(bytes.NewBufferString(h.Manifest))
if err != nil {
return errors.Wrapf(err, "unable to build kubernetes object for %s hook %s", hook, h.Path)
}
if _, err := client.Create(resources); err != nil {
return errors.Wrapf(err, "warning: Hook %s %s failed", hook, h.Path)
}
if err := client.WatchUntilReady(resources, timeout); err != nil {
// If a hook is failed, checkout the annotation of the hook to determine whether the hook should be deleted
// under failed condition. If so, then clear the corresponding resource object in the hook
if err := deleteHookByPolicy(client, h, hooks.HookFailed); err != nil {
return err
}
return err
}
}
// If all hooks are succeeded, checkout the annotation of each hook to determine whether the hook should be deleted
// under succeeded condition. If so, then clear the corresponding resource object in each hook
for _, h := range executingHooks {
if err := deleteHookByPolicy(client, h, hooks.HookSucceeded); err != nil {
return err
}
h.LastRun = time.Now()
}
return nil
}
// deleteHookByPolicy deletes a hook if the hook policy instructs it to
func deleteHookByPolicy(client kube.Interface, h *release.Hook, policy string) error {
if hookHasDeletePolicy(h, policy) {
resources, err := client.Build(bytes.NewBufferString(h.Manifest))
if err != nil {
return errors.Wrapf(err, "unable to build kubernetes object for deleting hook %s", h.Path)
}
_, errs := client.Delete(resources)
if len(errs) > 0 {
return errors.New(joinErrors(errs))
}
}
return nil
}
func joinErrors(errs []error) string {
es := make([]string, 0, len(errs))
for _, e := range errs {
es = append(es, e.Error())
}
return strings.Join(es, "; ")
}

@ -89,7 +89,7 @@ var manifestWithTestHook = `kind: Pod
metadata:
name: finding-nemo,
annotations:
"helm.sh/hook": test-success
"helm.sh/hook": test
spec:
containers:
- name: nemo-test
@ -231,7 +231,7 @@ func namedReleaseStub(name string, status release.Status) *release.Release {
Path: "finding-nemo",
Manifest: manifestWithTestHook,
Events: []release.HookEvent{
release.HookReleaseTestSuccess,
release.HookTest,
},
},
},

@ -0,0 +1,133 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package action
import (
"bytes"
"sort"
"time"
"github.com/pkg/errors"
"helm.sh/helm/pkg/release"
)
// execHook executes all of the hooks for the given hook event.
func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent, timeout time.Duration) error {
executingHooks := []*release.Hook{}
for _, h := range rl.Hooks {
for _, e := range h.Events {
if e == hook {
executingHooks = append(executingHooks, h)
}
}
}
sort.Sort(hookByWeight(executingHooks))
for _, h := range executingHooks {
if err := cfg.deleteHookByPolicy(h, release.HookBeforeHookCreation); err != nil {
return err
}
resources, err := cfg.KubeClient.Build(bytes.NewBufferString(h.Manifest))
if err != nil {
return errors.Wrapf(err, "unable to build kubernetes object for %s hook %s", hook, h.Path)
}
// Record the time at which the hook was applied to the cluster
h.LastRun = release.HookExecution{
StartedAt: time.Now(),
Phase: release.HookPhaseRunning,
}
cfg.recordRelease(rl)
// As long as the implementation of WatchUntilReady does not panic, HookPhaseFailed or HookPhaseSucceeded
// should always be set by this function. If we fail to do that for any reason, then HookPhaseUnknown is
// the most appropriate value to surface.
h.LastRun.Phase = release.HookPhaseUnknown
// Create hook resources
if _, err := cfg.KubeClient.Create(resources); err != nil {
h.LastRun.CompletedAt = time.Now()
h.LastRun.Phase = release.HookPhaseFailed
return errors.Wrapf(err, "warning: Hook %s %s failed", hook, h.Path)
}
// Watch hook resources until they have completed
err = cfg.KubeClient.WatchUntilReady(resources, timeout)
// Note the time of success/failure
h.LastRun.CompletedAt = time.Now()
// Mark hook as succeeded or failed
if err != nil {
h.LastRun.Phase = release.HookPhaseFailed
// If a hook is failed, check the annotation of the hook to determine whether the hook should be deleted
// under failed condition. If so, then clear the corresponding resource object in the hook
if err := cfg.deleteHookByPolicy(h, release.HookFailed); err != nil {
return err
}
return err
}
h.LastRun.Phase = release.HookPhaseSucceeded
}
// If all hooks are successful, check the annotation of each hook to determine whether the hook should be deleted
// under succeeded condition. If so, then clear the corresponding resource object in each hook
for _, h := range executingHooks {
if err := cfg.deleteHookByPolicy(h, release.HookSucceeded); err != nil {
return err
}
}
return nil
}
// hookByWeight is a sorter for hooks
type hookByWeight []*release.Hook
func (x hookByWeight) Len() int { return len(x) }
func (x hookByWeight) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
func (x hookByWeight) Less(i, j int) bool {
if x[i].Weight == x[j].Weight {
return x[i].Name < x[j].Name
}
return x[i].Weight < x[j].Weight
}
// deleteHookByPolicy deletes a hook if the hook policy instructs it to
func (cfg *Configuration) deleteHookByPolicy(h *release.Hook, policy release.HookDeletePolicy) error {
if hookHasDeletePolicy(h, policy) {
resources, err := cfg.KubeClient.Build(bytes.NewBufferString(h.Manifest))
if err != nil {
return errors.Wrapf(err, "unable to build kubernetes object for deleting hook %s", h.Path)
}
_, errs := cfg.KubeClient.Delete(resources)
return errors.New(joinErrors(errs))
}
return nil
}
// hookHasDeletePolicy determines whether the defined hook deletion policy matches the hook deletion polices
// supported by helm. If so, mark the hook as one should be deleted.
func hookHasDeletePolicy(h *release.Hook, policy release.HookDeletePolicy) bool {
for _, v := range h.DeletePolicies {
if policy == v {
return true
}
}
return false
}

@ -37,7 +37,6 @@ import (
"helm.sh/helm/pkg/engine"
"helm.sh/helm/pkg/getter"
"helm.sh/helm/pkg/helmpath"
"helm.sh/helm/pkg/hooks"
kubefake "helm.sh/helm/pkg/kube/fake"
"helm.sh/helm/pkg/release"
"helm.sh/helm/pkg/releaseutil"
@ -169,7 +168,7 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release.
return rel, nil
}
// If Replace is true, we need to supersede the last release.
// If Replace is true, we need to supercede the last release.
if i.Replace {
if err := i.replaceRelease(rel); err != nil {
return nil, err
@ -187,7 +186,7 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release.
// pre-install hooks
if !i.DisableHooks {
if err := execHooks(i.cfg.KubeClient, rel.Hooks, hooks.PreInstall, i.Timeout); err != nil {
if err := i.cfg.execHook(rel, release.HookPreInstall, i.Timeout); err != nil {
return i.failRelease(rel, fmt.Errorf("failed pre-install: %s", err))
}
}
@ -207,7 +206,7 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release.
}
if !i.DisableHooks {
if err := execHooks(i.cfg.KubeClient, rel.Hooks, hooks.PostInstall, i.Timeout); err != nil {
if err := i.cfg.execHook(rel, release.HookPostInstall, i.Timeout); err != nil {
return i.failRelease(rel, fmt.Errorf("failed post-install: %s", err))
}
}
@ -446,41 +445,6 @@ func ensureDirectoryForFile(file string) error {
return os.MkdirAll(baseDir, defaultDirectoryPermission)
}
// deletePolices represents a mapping between the key in the annotation for label deleting policy and its real meaning
// FIXME: Can we refactor this out?
var deletePolices = map[string]release.HookDeletePolicy{
hooks.HookSucceeded: release.HookSucceeded,
hooks.HookFailed: release.HookFailed,
hooks.BeforeHookCreation: release.HookBeforeHookCreation,
}
// hookHasDeletePolicy determines whether the defined hook deletion policy matches the hook deletion polices
// supported by helm. If so, mark the hook as one should be deleted.
func hookHasDeletePolicy(h *release.Hook, policy string) bool {
dp, ok := deletePolices[policy]
if !ok {
return false
}
for _, v := range h.DeletePolicies {
if dp == v {
return true
}
}
return false
}
// hookByWeight is a sorter for hooks
type hookByWeight []*release.Hook
func (x hookByWeight) Len() int { return len(x) }
func (x hookByWeight) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
func (x hookByWeight) Less(i, j int) bool {
if x[i].Weight == x[j].Weight {
return x[i].Name < x[j].Name
}
return x[i].Weight < x[j].Weight
}
// NameAndChart returns the name and chart that should be used.
//
// This will read the flags and handle name generation if necessary.

@ -177,7 +177,7 @@ func TestInstallRelease_DryRun(t *testing.T) {
_, err = instAction.cfg.Releases.Get(res.Name, res.Version)
is.Error(err)
is.Len(res.Hooks, 1)
is.True(res.Hooks[0].LastRun.IsZero(), "expect hook to not be marked as run")
is.True(res.Hooks[0].LastRun.CompletedAt.IsZero(), "expect hook to not be marked as run")
is.Equal(res.Info.Description, "Dry run complete")
}
@ -194,7 +194,7 @@ func TestInstallRelease_NoHooks(t *testing.T) {
t.Fatalf("Failed install: %s", err)
}
is.True(res.Hooks[0].LastRun.IsZero(), "hooks should not run with no-hooks")
is.True(res.Hooks[0].LastRun.CompletedAt.IsZero(), "hooks should not run with no-hooks")
}
func TestInstallRelease_FailedHooks(t *testing.T) {
@ -209,7 +209,7 @@ func TestInstallRelease_FailedHooks(t *testing.T) {
res, err := instAction.Run(buildChart(), vals)
is.Error(err)
is.Contains(res.Info.Description, "failed post-install")
is.Equal(res.Info.Status, release.StatusFailed)
is.Equal(release.StatusFailed, res.Info.Status)
}
func TestInstallRelease_ReplaceRelease(t *testing.T) {

@ -23,9 +23,6 @@ import (
"strings"
"text/tabwriter"
"github.com/gosuri/uitable"
"github.com/gosuri/uitable/util/strutil"
"helm.sh/helm/pkg/release"
)
@ -48,12 +45,21 @@ func PrintRelease(out io.Writer, rel *release.Release) {
fmt.Fprintf(w, "RESOURCES:\n%s\n", re.ReplaceAllString(rel.Info.Resources, "\t"))
w.Flush()
}
if rel.Info.LastTestSuiteRun != nil {
lastRun := rel.Info.LastTestSuiteRun
fmt.Fprintf(out, "TEST SUITE:\n%s\n%s\n\n%s\n",
fmt.Sprintf("Last Started: %s", lastRun.StartedAt),
fmt.Sprintf("Last Completed: %s", lastRun.CompletedAt),
formatTestResults(lastRun.Results))
executions := executionsByHookEvent(rel)
if tests, ok := executions[release.HookTest]; ok {
for _, h := range tests {
// Don't print anything if hook has not been initiated
if h.LastRun.StartedAt.IsZero() {
continue
}
fmt.Fprintf(out, "TEST SUITE: %s\n%s\n%s\n%s\n\n",
h.Name,
fmt.Sprintf("Last Started: %s", h.LastRun.StartedAt),
fmt.Sprintf("Last Completed: %s", h.LastRun.CompletedAt),
fmt.Sprintf("Phase: %s", h.LastRun.Phase),
)
}
}
if strings.EqualFold(rel.Info.Description, "Dry run complete") {
@ -65,18 +71,16 @@ func PrintRelease(out io.Writer, rel *release.Release) {
}
}
func formatTestResults(results []*release.TestRun) string {
tbl := uitable.New()
tbl.MaxColWidth = 50
tbl.AddRow("TEST", "STATUS", "INFO", "STARTED", "COMPLETED")
for i := 0; i < len(results); i++ {
r := results[i]
n := r.Name
s := strutil.PadRight(r.Status.String(), 10, ' ')
i := r.Info
ts := r.StartedAt
tc := r.CompletedAt
tbl.AddRow(n, s, i, ts, tc)
func executionsByHookEvent(rel *release.Release) map[release.HookEvent][]*release.Hook {
result := make(map[release.HookEvent][]*release.Hook)
for _, h := range rel.Hooks {
for _, e := range h.Events {
executions, ok := result[e]
if !ok {
executions = []*release.Hook{}
}
result[e] = append(executions, h)
}
}
return tbl.String()
return result
}

@ -22,7 +22,6 @@ import (
"github.com/pkg/errors"
"helm.sh/helm/pkg/release"
reltesting "helm.sh/helm/pkg/releasetesting"
)
// ReleaseTesting is the action for testing a release.
@ -43,52 +42,21 @@ func NewReleaseTesting(cfg *Configuration) *ReleaseTesting {
}
// Run executes 'helm test' against the given release.
func (r *ReleaseTesting) Run(name string) (<-chan *release.TestReleaseResponse, <-chan error) {
errc := make(chan error, 1)
func (r *ReleaseTesting) Run(name string) error {
if err := validateReleaseName(name); err != nil {
errc <- errors.Errorf("releaseTest: Release name is invalid: %s", name)
return nil, errc
return errors.Errorf("releaseTest: Release name is invalid: %s", name)
}
// finds the non-deleted release with the given name
rel, err := r.cfg.Releases.Last(name)
if err != nil {
errc <- err
return nil, errc
return err
}
ch := make(chan *release.TestReleaseResponse, 1)
testEnv := &reltesting.Environment{
Namespace: rel.Namespace,
KubeClient: r.cfg.KubeClient,
Timeout: r.Timeout,
Messages: ch,
if err := r.cfg.execHook(rel, release.HookTest, r.Timeout); err != nil {
r.cfg.Releases.Update(rel)
return err
}
r.cfg.Log("running tests for release %s", rel.Name)
tSuite := reltesting.NewTestSuite(rel)
go func() {
defer close(errc)
defer close(ch)
if err := tSuite.Run(testEnv); err != nil {
errc <- errors.Wrapf(err, "error running test suite for %s", rel.Name)
return
}
rel.Info.LastTestSuiteRun = &release.TestSuite{
StartedAt: tSuite.StartedAt,
CompletedAt: tSuite.CompletedAt,
Results: tSuite.Results,
}
if r.Cleanup {
testEnv.DeleteTestPods(tSuite.TestManifests)
}
if err := r.cfg.Releases.Update(rel); err != nil {
r.cfg.Log("test: Failed to store updated release: %s", err)
}
}()
return ch, errc
return r.cfg.Releases.Update(rel)
}

@ -23,7 +23,6 @@ import (
"github.com/pkg/errors"
"helm.sh/helm/pkg/hooks"
"helm.sh/helm/pkg/release"
)
@ -147,7 +146,7 @@ func (r *Rollback) performRollback(currentRelease, targetRelease *release.Releas
// pre-rollback hooks
if !r.DisableHooks {
if err := execHooks(r.cfg.KubeClient, targetRelease.Hooks, hooks.PreRollback, r.Timeout); err != nil {
if err := r.cfg.execHook(targetRelease, release.HookPreRollback, r.Timeout); err != nil {
return targetRelease, err
}
} else {
@ -188,7 +187,7 @@ func (r *Rollback) performRollback(currentRelease, targetRelease *release.Releas
// post-rollback hooks
if !r.DisableHooks {
if err := execHooks(r.cfg.KubeClient, targetRelease.Hooks, hooks.PostRollback, r.Timeout); err != nil {
if err := r.cfg.execHook(targetRelease, release.HookPostRollback, r.Timeout); err != nil {
return targetRelease, err
}
}

@ -22,7 +22,6 @@ import (
"github.com/pkg/errors"
"helm.sh/helm/pkg/hooks"
"helm.sh/helm/pkg/release"
"helm.sh/helm/pkg/releaseutil"
)
@ -91,7 +90,7 @@ func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error)
res := &release.UninstallReleaseResponse{Release: rel}
if !u.DisableHooks {
if err := execHooks(u.cfg.KubeClient, rel.Hooks, hooks.PreDelete, u.Timeout); err != nil {
if err := u.cfg.execHook(rel, release.HookPreDelete, u.Timeout); err != nil {
return res, err
}
} else {
@ -108,7 +107,7 @@ func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error)
res.Info = kept
if !u.DisableHooks {
if err := execHooks(u.cfg.KubeClient, rel.Hooks, hooks.PostDelete, u.Timeout); err != nil {
if err := u.cfg.execHook(rel, release.HookPostDelete, u.Timeout); err != nil {
errs = append(errs, err)
}
}
@ -150,6 +149,14 @@ func (u *Uninstall) purgeReleases(rels ...*release.Release) error {
return nil
}
func joinErrors(errs []error) string {
es := make([]string, 0, len(errs))
for _, e := range errs {
es = append(es, e.Error())
}
return strings.Join(es, "; ")
}
// deleteRelease deletes the release and returns manifests that were kept in the deletion process
func (u *Uninstall) deleteRelease(rel *release.Release) (string, []error) {
caps, err := u.cfg.getCapabilities()

@ -26,7 +26,6 @@ import (
"helm.sh/helm/pkg/chart"
"helm.sh/helm/pkg/chartutil"
"helm.sh/helm/pkg/hooks"
"helm.sh/helm/pkg/kube"
"helm.sh/helm/pkg/release"
"helm.sh/helm/pkg/releaseutil"
@ -210,7 +209,7 @@ func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Relea
// pre-upgrade hooks
if !u.DisableHooks {
if err := execHooks(u.cfg.KubeClient, upgradedRelease.Hooks, hooks.PreUpgrade, u.Timeout); err != nil {
if err := u.cfg.execHook(upgradedRelease, release.HookPreUpgrade, u.Timeout); err != nil {
return u.failRelease(upgradedRelease, fmt.Errorf("pre-upgrade hooks failed: %s", err))
}
} else {
@ -242,7 +241,7 @@ func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Relea
// post-upgrade hooks
if !u.DisableHooks {
if err := execHooks(u.cfg.KubeClient, upgradedRelease.Hooks, hooks.PostUpgrade, u.Timeout); err != nil {
if err := u.cfg.execHook(upgradedRelease, release.HookPostUpgrade, u.Timeout); err != nil {
return u.failRelease(upgradedRelease, fmt.Errorf("post-upgrade hooks failed: %s", err))
}
}

@ -1,67 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package hooks
import (
"helm.sh/helm/pkg/release"
)
// HookAnno is the label name for a hook
const HookAnno = "helm.sh/hook"
// HookWeightAnno is the label name for a hook weight
const HookWeightAnno = "helm.sh/hook-weight"
// HookDeleteAnno is the label name for the delete policy for a hook
const HookDeleteAnno = "helm.sh/hook-delete-policy"
// Types of hooks
const (
PreInstall = "pre-install"
PostInstall = "post-install"
PreDelete = "pre-delete"
PostDelete = "post-delete"
PreUpgrade = "pre-upgrade"
PostUpgrade = "post-upgrade"
PreRollback = "pre-rollback"
PostRollback = "post-rollback"
ReleaseTestSuccess = "test-success"
ReleaseTestFailure = "test-failure"
)
// Type of policy for deleting the hook
const (
HookSucceeded = "hook-succeeded"
HookFailed = "hook-failed"
BeforeHookCreation = "before-hook-creation"
)
// FilterTestHooks filters the list of hooks are returns only testing hooks.
func FilterTestHooks(hooks []*release.Hook) []*release.Hook {
testHooks := []*release.Hook{}
for _, h := range hooks {
for _, e := range h.Events {
if e == release.HookReleaseTestSuccess || e == release.HookReleaseTestFailure {
testHooks = append(testHooks, h)
continue
}
}
}
return testHooks
}

@ -234,6 +234,8 @@ func (c *Client) watchTimeout(t time.Duration) func(*resource.Info) error {
//
// - Jobs: A job is marked "Ready" when it has successfully completed. This is
// ascertained by watching the Status fields in a job's output.
// - Pods: A pod is marked "Ready" when it has successfully completed. This is
// ascertained by watching the status.phase field in a pod's output.
//
// Handling for other kinds will be added as necessary.
func (c *Client) WatchUntilReady(resources ResourceList, timeout time.Duration) error {
@ -393,8 +395,11 @@ func (c *Client) watchUntilReady(timeout time.Duration, info *resource.Info) err
// the status go into a good state. For other types, like ReplicaSet
// we don't really do anything to support these as hooks.
c.Log("Add/Modify event for %s: %v", info.Name, e.Type)
if kind == "Job" {
switch kind {
case "Job":
return c.waitForJob(obj, info.Name)
case "Pod":
return c.waitForPodSuccess(obj, info.Name)
}
return true, nil
case watch.Deleted:
@ -432,6 +437,30 @@ func (c *Client) waitForJob(obj runtime.Object, name string) (bool, error) {
return false, nil
}
// waitForPodSuccess is a helper that waits for a pod to complete.
//
// This operates on an event returned from a watcher.
func (c *Client) waitForPodSuccess(obj runtime.Object, name string) (bool, error) {
o, ok := obj.(*v1.Pod)
if !ok {
return true, errors.Errorf("expected %s to be a *v1.Pod, got %T", name, obj)
}
switch o.Status.Phase {
case v1.PodSucceeded:
fmt.Printf("Pod %s succeeded\n", o.Name)
return true, nil
case v1.PodFailed:
return true, errors.Errorf("pod %s failed", o.Name)
case v1.PodPending:
fmt.Printf("Pod %s pending\n", o.Name)
case v1.PodRunning:
fmt.Printf("Pod %s running\n", o.Name)
}
return false, nil
}
// scrubValidationError removes kubectl info from the message.
func scrubValidationError(err error) error {
if err == nil {

@ -23,7 +23,7 @@ import (
v1 "k8s.io/api/core/v1"
)
// KubernetesClient represents a client capable of communicating with the Kubernetes API.
// Interface represents a client capable of communicating with the Kubernetes API.
//
// A KubernetesClient must be concurrency safe.
type Interface interface {
@ -37,7 +37,8 @@ type Interface interface {
// Watch the resource in reader until it is "ready". This method
//
// For Jobs, "ready" means the job ran to completion (excited without error).
// For Jobs, "ready" means the Job ran to completion (exited without error).
// For Pods, "ready" means the Pod phase is marked "succeeded".
// For all other kinds, it means the kind was created or modified without
// error.
WatchUntilReady(resources ResourceList, timeout time.Duration) error

@ -1,10 +1,11 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
@ -15,23 +16,24 @@ limitations under the License.
package release
import "time"
import (
"time"
)
// HookEvent specifies the hook event
type HookEvent string
// Hook event types
const (
HookPreInstall HookEvent = "pre-install"
HookPostInstall HookEvent = "post-install"
HookPreDelete HookEvent = "pre-delete"
HookPostDelete HookEvent = "post-delete"
HookPreUpgrade HookEvent = "pre-upgrade"
HookPostUpgrade HookEvent = "post-upgrade"
HookPreRollback HookEvent = "pre-rollback"
HookPostRollback HookEvent = "post-rollback"
HookReleaseTestSuccess HookEvent = "release-test-success"
HookReleaseTestFailure HookEvent = "release-test-failure"
HookPreInstall HookEvent = "pre-install"
HookPostInstall HookEvent = "post-install"
HookPreDelete HookEvent = "pre-delete"
HookPostDelete HookEvent = "post-delete"
HookPreUpgrade HookEvent = "pre-upgrade"
HookPostUpgrade HookEvent = "post-upgrade"
HookPreRollback HookEvent = "pre-rollback"
HookPostRollback HookEvent = "post-rollback"
HookTest HookEvent = "test"
)
func (x HookEvent) String() string { return string(x) }
@ -41,13 +43,22 @@ type HookDeletePolicy string
// Hook delete policy types
const (
HookSucceeded HookDeletePolicy = "succeeded"
HookFailed HookDeletePolicy = "failed"
HookSucceeded HookDeletePolicy = "hook-succeeded"
HookFailed HookDeletePolicy = "hook-failed"
HookBeforeHookCreation HookDeletePolicy = "before-hook-creation"
)
func (x HookDeletePolicy) String() string { return string(x) }
// HookAnnotation is the label name for a hook
const HookAnnotation = "helm.sh/hook"
// HookWeightAnnotation is the label name for a hook weight
const HookWeightAnnotation = "helm.sh/hook-weight"
// HookDeleteAnnotation is the label name for the delete policy for a hook
const HookDeleteAnnotation = "helm.sh/hook-delete-policy"
// Hook defines a hook object.
type Hook struct {
Name string `json:"name,omitempty"`
@ -60,9 +71,36 @@ type Hook struct {
// Events are the events that this hook fires on.
Events []HookEvent `json:"events,omitempty"`
// LastRun indicates the date/time this was last run.
LastRun time.Time `json:"last_run,omitempty"`
LastRun HookExecution `json:"last_run,omitempty"`
// Weight indicates the sort order for execution among similar Hook type
Weight int `json:"weight,omitempty"`
// DeletePolicies are the policies that indicate when to delete the hook
DeletePolicies []HookDeletePolicy `json:"delete_policies,omitempty"`
}
// A HookExecution records the result for the last execution of a hook for a given release.
type HookExecution struct {
// StartedAt indicates the date/time this hook was started
StartedAt time.Time `json:"started_at,omitempty"`
// CompletedAt indicates the date/time this hook was completed.
CompletedAt time.Time `json:"completed_at,omitempty"`
// Phase indicates whether the hook completed successfully
Phase HookPhase `json:"phase"`
}
// A HookPhase indicates the state of a hook execution
type HookPhase string
const (
// HookPhaseUnknown indicates that a hook is in an unknown state
HookPhaseUnknown HookPhase = "Unknown"
// HookPhaseRunning indicates that a hook is currently executing
HookPhaseRunning HookPhase = "Running"
// HookPhaseSucceeded indicates that hook execution succeeded
HookPhaseSucceeded HookPhase = "Succeeded"
// HookPhaseFailed indicates that hook execution failed
HookPhaseFailed HookPhase = "Failed"
)
// Strng converts a hook phase to a printable string
func (x HookPhase) String() string { return string(x) }

@ -33,6 +33,4 @@ type Info struct {
Resources string `json:"resources,omitempty"`
// Contains the rendered templates/NOTES.txt if available
Notes string `json:"notes,omitempty"`
// LastTestSuiteRun provides results on the last test run on a release
LastTestSuiteRun *TestSuite `json:"last_test_suite_run,omitempty"`
}

@ -40,12 +40,11 @@ metadata:
// MockReleaseOptions allows for user-configurable options on mock release objects.
type MockReleaseOptions struct {
Name string
Version int
Chart *chart.Chart
Status Status
Namespace string
TestSuiteResults []*TestRun
Name string
Version int
Chart *chart.Chart
Status Status
Namespace string
}
// Mock creates a mock release object based on options set by MockReleaseOptions. This function should typically not be used outside of testing.
@ -93,14 +92,6 @@ func Mock(opts *MockReleaseOptions) *Release {
Description: "Release mock",
}
if len(opts.TestSuiteResults) > 0 {
info.LastTestSuiteRun = &TestSuite{
StartedAt: date,
CompletedAt: date,
Results: opts.TestSuiteResults,
}
}
return &Release{
Name: name,
Info: info,
@ -114,7 +105,7 @@ func Mock(opts *MockReleaseOptions) *Release {
Kind: "Job",
Path: "pre-install-hook.yaml",
Manifest: MockHookTemplate,
LastRun: date,
LastRun: HookExecution{},
Events: []HookEvent{HookPreInstall},
},
},

@ -32,9 +32,3 @@ type UninstallReleaseResponse struct {
// Info is an uninstall message
Info string `json:"info,omitempty"`
}
// TestReleaseResponse represents a message from executing a test
type TestReleaseResponse struct {
Msg string `json:"msg,omitempty"`
Status TestRunStatus `json:"status,omitempty"`
}

@ -1,41 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package release
import "time"
// TestRunStatus is the status of a test run
type TestRunStatus string
// Indicates the results of a test run
const (
TestRunUnknown TestRunStatus = "unknown"
TestRunSuccess TestRunStatus = "success"
TestRunFailure TestRunStatus = "failure"
TestRunRunning TestRunStatus = "running"
)
// Strng converts a test run status to a printable string
func (x TestRunStatus) String() string { return string(x) }
// TestRun describes the run of a test
type TestRun struct {
Name string `json:"name,omitempty"`
Status TestRunStatus `json:"status,omitempty"`
Info string `json:"info,omitempty"`
StartedAt time.Time `json:"started_at,omitempty"`
CompletedAt time.Time `json:"completed_at,omitempty"`
}

@ -1,28 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package release
import "time"
// TestSuite comprises of the last run of the pre-defined test suite of a release version
type TestSuite struct {
// StartedAt indicates the date/time this test suite was kicked off
StartedAt time.Time `json:"started_at,omitempty"`
// CompletedAt indicates the date/time this test suite was completed
CompletedAt time.Time `json:"completed_at,omitempty"`
// Results are the results of each segment of the test
Results []*TestRun `json:"results,omitempty"`
}

@ -1,129 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package releasetesting
import (
"bytes"
"fmt"
"log"
"time"
v1 "k8s.io/api/core/v1"
"helm.sh/helm/pkg/kube"
"helm.sh/helm/pkg/release"
)
// Environment encapsulates information about where test suite executes and returns results
type Environment struct {
Namespace string
KubeClient kube.Interface
Messages chan *release.TestReleaseResponse
Timeout time.Duration
}
func (env *Environment) createTestPod(test *test) error {
resources, err := env.KubeClient.Build(bytes.NewBufferString(test.manifest))
if err != nil {
return err
}
if _, err := env.KubeClient.Create(resources); err != nil {
test.result.Info = err.Error()
test.result.Status = release.TestRunFailure
return err
}
return nil
}
func (env *Environment) getTestPodStatus(test *test) (v1.PodPhase, error) {
status, err := env.KubeClient.WaitAndGetCompletedPodPhase(test.name, env.Timeout)
if err != nil {
log.Printf("Error getting status for pod %s: %s", test.result.Name, err)
test.result.Info = err.Error()
test.result.Status = release.TestRunUnknown
return status, err
}
return status, err
}
func (env *Environment) streamResult(r *release.TestRun) error {
switch r.Status {
case release.TestRunSuccess:
if err := env.streamSuccess(r.Name); err != nil {
return err
}
case release.TestRunFailure:
if err := env.streamFailed(r.Name); err != nil {
return err
}
default:
if err := env.streamUnknown(r.Name, r.Info); err != nil {
return err
}
}
return nil
}
func (env *Environment) streamRunning(name string) error {
msg := "RUNNING: " + name
return env.streamMessage(msg, release.TestRunRunning)
}
func (env *Environment) streamError(info string) error {
msg := "ERROR: " + info
return env.streamMessage(msg, release.TestRunFailure)
}
func (env *Environment) streamFailed(name string) error {
msg := fmt.Sprintf("FAILED: %s, run `kubectl logs %s --namespace %s` for more info", name, name, env.Namespace)
return env.streamMessage(msg, release.TestRunFailure)
}
func (env *Environment) streamSuccess(name string) error {
msg := fmt.Sprintf("PASSED: %s", name)
return env.streamMessage(msg, release.TestRunSuccess)
}
func (env *Environment) streamUnknown(name, info string) error {
msg := fmt.Sprintf("UNKNOWN: %s: %s", name, info)
return env.streamMessage(msg, release.TestRunUnknown)
}
func (env *Environment) streamMessage(msg string, status release.TestRunStatus) error {
resp := &release.TestReleaseResponse{Msg: msg, Status: status}
env.Messages <- resp
return nil
}
// DeleteTestPods deletes resources given in testManifests
func (env *Environment) DeleteTestPods(testManifests []string) {
for _, testManifest := range testManifests {
resources, err := env.KubeClient.Build(bytes.NewBufferString(testManifest))
if err != nil {
env.streamError(err.Error())
}
_, errs := env.KubeClient.Delete(resources)
if err != nil {
for _, e := range errs {
env.streamError(e.Error())
}
}
}
}

@ -1,71 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package releasetesting
import (
"testing"
"github.com/pkg/errors"
"helm.sh/helm/pkg/release"
)
func TestCreateTestPodSuccess(t *testing.T) {
env := testEnvFixture()
test := testFixture()
if err := env.createTestPod(test); err != nil {
t.Errorf("Expected no error, got an error: %s", err)
}
}
func TestCreateTestPodFailure(t *testing.T) {
env := testEnvFixture()
env.KubeClient = &mockKubeClient{
err: errors.New("We ran out of budget and couldn't create finding-nemo"),
}
test := testFixture()
if err := env.createTestPod(test); err == nil {
t.Errorf("Expected error, got no error")
}
if test.result.Info == "" {
t.Errorf("Expected error to be saved in test result info but found empty string")
}
if test.result.Status != release.TestRunFailure {
t.Errorf("Expected test result status to be failure but got: %v", test.result.Status)
}
}
func TestStreamMessage(t *testing.T) {
env := testEnvFixture()
defer close(env.Messages)
expectedMessage := "testing streamMessage"
expectedStatus := release.TestRunSuccess
if err := env.streamMessage(expectedMessage, expectedStatus); err != nil {
t.Errorf("Expected no errors, got: %s", err)
}
got := <-env.Messages
if got.Msg != expectedMessage {
t.Errorf("Expected message: %s, got: %s", expectedMessage, got.Msg)
}
if got.Status != expectedStatus {
t.Errorf("Expected status: %v, got: %v", expectedStatus, got.Status)
}
}

@ -1,187 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package releasetesting
import (
"strings"
"time"
"github.com/pkg/errors"
v1 "k8s.io/api/core/v1"
"sigs.k8s.io/yaml"
"helm.sh/helm/pkg/hooks"
"helm.sh/helm/pkg/release"
util "helm.sh/helm/pkg/releaseutil"
)
// TestSuite what tests are run, results, and metadata
type TestSuite struct {
StartedAt time.Time
CompletedAt time.Time
TestManifests []string
Results []*release.TestRun
}
type test struct {
name string
manifest string
expectedSuccess bool
result *release.TestRun
}
// NewTestSuite takes a release object and returns a TestSuite object with test definitions
// extracted from the release
func NewTestSuite(rel *release.Release) *TestSuite {
return &TestSuite{
TestManifests: extractTestManifestsFromHooks(rel.Hooks),
Results: []*release.TestRun{},
}
}
// Run executes tests in a test suite and stores a result within a given environment
func (ts *TestSuite) Run(env *Environment) error {
ts.StartedAt = time.Now()
if len(ts.TestManifests) == 0 {
// TODO: make this better, adding test run status on test suite is weird
env.streamMessage("No Tests Found", release.TestRunUnknown)
}
for _, testManifest := range ts.TestManifests {
test, err := newTest(testManifest)
if err != nil {
return err
}
test.result.StartedAt = time.Now()
if err := env.streamRunning(test.name); err != nil {
return err
}
test.result.Status = release.TestRunRunning
resourceCreated := true
if err := env.createTestPod(test); err != nil {
resourceCreated = false
if streamErr := env.streamError(test.result.Info); streamErr != nil {
return err
}
}
resourceCleanExit := true
status := v1.PodUnknown
if resourceCreated {
status, err = env.getTestPodStatus(test)
if err != nil {
resourceCleanExit = false
if streamErr := env.streamError(test.result.Info); streamErr != nil {
return streamErr
}
}
}
if resourceCreated && resourceCleanExit {
if err := test.assignTestResult(status); err != nil {
return err
}
if err := env.streamResult(test.result); err != nil {
return err
}
}
test.result.CompletedAt = time.Now()
ts.Results = append(ts.Results, test.result)
}
ts.CompletedAt = time.Now()
return nil
}
func (t *test) assignTestResult(podStatus v1.PodPhase) error {
switch podStatus {
case v1.PodSucceeded:
if t.expectedSuccess {
t.result.Status = release.TestRunSuccess
} else {
t.result.Status = release.TestRunFailure
}
case v1.PodFailed:
if !t.expectedSuccess {
t.result.Status = release.TestRunSuccess
} else {
t.result.Status = release.TestRunFailure
}
default:
t.result.Status = release.TestRunUnknown
}
return nil
}
func expectedSuccess(hookTypes []string) (bool, error) {
for _, hookType := range hookTypes {
hookType = strings.ToLower(strings.TrimSpace(hookType))
if hookType == hooks.ReleaseTestSuccess {
return true, nil
} else if hookType == hooks.ReleaseTestFailure {
return false, nil
}
}
return false, errors.Errorf("no %s or %s hook found", hooks.ReleaseTestSuccess, hooks.ReleaseTestFailure)
}
func extractTestManifestsFromHooks(h []*release.Hook) []string {
testHooks := hooks.FilterTestHooks(h)
tests := []string{}
for _, h := range testHooks {
individualTests := util.SplitManifests(h.Manifest)
for _, t := range individualTests {
tests = append(tests, t)
}
}
return tests
}
func newTest(testManifest string) (*test, error) {
var sh util.SimpleHead
err := yaml.Unmarshal([]byte(testManifest), &sh)
if err != nil {
return nil, err
}
if sh.Kind != "Pod" {
return nil, errors.Errorf("%s is not a pod", sh.Metadata.Name)
}
hookTypes := sh.Metadata.Annotations[hooks.HookAnno]
expected, err := expectedSuccess(strings.Split(hookTypes, ","))
if err != nil {
return nil, err
}
name := strings.TrimSuffix(sh.Metadata.Name, ",")
return &test{
name: name,
manifest: testManifest,
expectedSuccess: expected,
result: &release.TestRun{
Name: name,
},
}, nil
}

@ -1,259 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package releasetesting
import (
"io/ioutil"
"testing"
"time"
v1 "k8s.io/api/core/v1"
"helm.sh/helm/pkg/kube"
"helm.sh/helm/pkg/kube/fake"
"helm.sh/helm/pkg/release"
)
const manifestWithTestSuccessHook = `
apiVersion: v1
kind: Pod
metadata:
name: finding-nemo,
annotations:
"helm.sh/hook": test-success
spec:
containers:
- name: nemo-test
image: fake-image
cmd: fake-command
`
const manifestWithTestFailureHook = `
apiVersion: v1
kind: Pod
metadata:
name: gold-rush,
annotations:
"helm.sh/hook": test-failure
spec:
containers:
- name: gold-finding-test
image: fake-gold-finding-image
cmd: fake-gold-finding-command
`
const manifestWithInstallHooks = `apiVersion: v1
kind: ConfigMap
metadata:
name: test-cm
annotations:
"helm.sh/hook": post-install,pre-delete
data:
name: value
`
func TestRun(t *testing.T) {
testManifests := []string{manifestWithTestSuccessHook, manifestWithTestFailureHook}
ts := testSuiteFixture(testManifests)
env := testEnvFixture()
go func() {
defer close(env.Messages)
if err := ts.Run(env); err != nil {
t.Error(err)
}
}()
for i := 0; i <= 4; i++ {
<-env.Messages
}
if _, ok := <-env.Messages; ok {
t.Errorf("Expected 4 messages streamed")
}
if ts.StartedAt.IsZero() {
t.Errorf("Expected StartedAt to not be nil. Got: %v", ts.StartedAt)
}
if ts.CompletedAt.IsZero() {
t.Errorf("Expected CompletedAt to not be nil. Got: %v", ts.CompletedAt)
}
if len(ts.Results) != 2 {
t.Errorf("Expected 2 test result. Got %v", len(ts.Results))
}
result := ts.Results[0]
if result.StartedAt.IsZero() {
t.Errorf("Expected test StartedAt to not be nil. Got: %v", result.StartedAt)
}
if result.CompletedAt.IsZero() {
t.Errorf("Expected test CompletedAt to not be nil. Got: %v", result.CompletedAt)
}
if result.Name != "finding-nemo" {
t.Errorf("Expected test name to be finding-nemo. Got: %v", result.Name)
}
if result.Status != release.TestRunSuccess {
t.Errorf("Expected test result to be successful, got: %v", result.Status)
}
result2 := ts.Results[1]
if result2.StartedAt.IsZero() {
t.Errorf("Expected test StartedAt to not be nil. Got: %v", result2.StartedAt)
}
if result2.CompletedAt.IsZero() {
t.Errorf("Expected test CompletedAt to not be nil. Got: %v", result2.CompletedAt)
}
if result2.Name != "gold-rush" {
t.Errorf("Expected test name to be gold-rush, Got: %v", result2.Name)
}
if result2.Status != release.TestRunFailure {
t.Errorf("Expected test result to be successful, got: %v", result2.Status)
}
}
func TestRunEmptyTestSuite(t *testing.T) {
ts := testSuiteFixture([]string{})
env := testEnvFixture()
go func() {
defer close(env.Messages)
if err := ts.Run(env); err != nil {
t.Error(err)
}
}()
msg := <-env.Messages
if msg.Msg != "No Tests Found" {
t.Errorf("Expected message 'No Tests Found', Got: %v", msg.Msg)
}
for range env.Messages {
}
if ts.StartedAt.IsZero() {
t.Errorf("Expected StartedAt to not be nil. Got: %v", ts.StartedAt)
}
if ts.CompletedAt.IsZero() {
t.Errorf("Expected CompletedAt to not be nil. Got: %v", ts.CompletedAt)
}
if len(ts.Results) != 0 {
t.Errorf("Expected 0 test result. Got %v", len(ts.Results))
}
}
func TestRunSuccessWithTestFailureHook(t *testing.T) {
ts := testSuiteFixture([]string{manifestWithTestFailureHook})
env := testEnvFixture()
env.KubeClient = &mockKubeClient{podFail: true}
go func() {
defer close(env.Messages)
if err := ts.Run(env); err != nil {
t.Error(err)
}
}()
for i := 0; i <= 4; i++ {
<-env.Messages
}
if _, ok := <-env.Messages; ok {
t.Errorf("Expected 4 messages streamed")
}
if ts.StartedAt.IsZero() {
t.Errorf("Expected StartedAt to not be nil. Got: %v", ts.StartedAt)
}
if ts.CompletedAt.IsZero() {
t.Errorf("Expected CompletedAt to not be nil. Got: %v", ts.CompletedAt)
}
if len(ts.Results) != 1 {
t.Errorf("Expected 1 test result. Got %v", len(ts.Results))
}
result := ts.Results[0]
if result.StartedAt.IsZero() {
t.Errorf("Expected test StartedAt to not be nil. Got: %v", result.StartedAt)
}
if result.CompletedAt.IsZero() {
t.Errorf("Expected test CompletedAt to not be nil. Got: %v", result.CompletedAt)
}
if result.Name != "gold-rush" {
t.Errorf("Expected test name to be gold-rush, Got: %v", result.Name)
}
if result.Status != release.TestRunSuccess {
t.Errorf("Expected test result to be successful, got: %v", result.Status)
}
}
func TestExtractTestManifestsFromHooks(t *testing.T) {
testManifests := extractTestManifestsFromHooks(hooksStub)
if len(testManifests) != 1 {
t.Errorf("Expected 1 test manifest, Got: %v", len(testManifests))
}
}
var hooksStub = []*release.Hook{
{
Manifest: manifestWithTestSuccessHook,
Events: []release.HookEvent{
release.HookReleaseTestSuccess,
},
},
{
Manifest: manifestWithInstallHooks,
Events: []release.HookEvent{
release.HookPostInstall,
},
},
}
func testFixture() *test {
return &test{
manifest: manifestWithTestSuccessHook,
result: &release.TestRun{},
}
}
func testSuiteFixture(testManifests []string) *TestSuite {
testResults := []*release.TestRun{}
ts := &TestSuite{
TestManifests: testManifests,
Results: testResults,
}
return ts
}
func testEnvFixture() *Environment {
return &Environment{
Namespace: "default",
KubeClient: &mockKubeClient{PrintingKubeClient: fake.PrintingKubeClient{Out: ioutil.Discard}},
Timeout: 1,
Messages: make(chan *release.TestReleaseResponse, 1),
}
}
type mockKubeClient struct {
fake.PrintingKubeClient
podFail bool
err error
}
func (c *mockKubeClient) WaitAndGetCompletedPodPhase(_ string, _ time.Duration) (v1.PodPhase, error) {
if c.podFail {
return v1.PodFailed, nil
}
return v1.PodSucceeded, nil
}
func (c *mockKubeClient) Create(_ kube.ResourceList) (*kube.Result, error) { return nil, c.err }

@ -26,7 +26,6 @@ import (
"sigs.k8s.io/yaml"
"helm.sh/helm/pkg/chartutil"
"helm.sh/helm/pkg/hooks"
"helm.sh/helm/pkg/release"
)
@ -53,16 +52,17 @@ type result struct {
// TODO: Refactor this out. It's here because naming conventions were not followed through.
// So fix the Test hook names and then remove this.
var events = map[string]release.HookEvent{
hooks.PreInstall: release.HookPreInstall,
hooks.PostInstall: release.HookPostInstall,
hooks.PreDelete: release.HookPreDelete,
hooks.PostDelete: release.HookPostDelete,
hooks.PreUpgrade: release.HookPreUpgrade,
hooks.PostUpgrade: release.HookPostUpgrade,
hooks.PreRollback: release.HookPreRollback,
hooks.PostRollback: release.HookPostRollback,
hooks.ReleaseTestSuccess: release.HookReleaseTestSuccess,
hooks.ReleaseTestFailure: release.HookReleaseTestFailure,
release.HookPreInstall.String(): release.HookPreInstall,
release.HookPostInstall.String(): release.HookPostInstall,
release.HookPreDelete.String(): release.HookPreDelete,
release.HookPostDelete.String(): release.HookPostDelete,
release.HookPreUpgrade.String(): release.HookPreUpgrade,
release.HookPostUpgrade.String(): release.HookPostUpgrade,
release.HookPreRollback.String(): release.HookPreRollback,
release.HookPostRollback.String(): release.HookPostRollback,
release.HookTest.String(): release.HookTest,
// Support test-success for backward compatibility with Helm 2 tests
"test-success": release.HookTest,
}
// SortManifests takes a map of filename/YAML contents, splits the file
@ -142,7 +142,7 @@ func (file *manifestFile) sort(result *result) error {
continue
}
hookTypes, ok := entry.Metadata.Annotations[hooks.HookAnno]
hookTypes, ok := entry.Metadata.Annotations[release.HookAnnotation]
if !ok {
result.generic = append(result.generic, Manifest{
Name: file.path,
@ -182,7 +182,7 @@ func (file *manifestFile) sort(result *result) error {
result.hooks = append(result.hooks, h)
operateAnnotationValues(entry, hooks.HookDeleteAnno, func(value string) {
operateAnnotationValues(entry, release.HookDeleteAnnotation, func(value string) {
h.DeletePolicies = append(h.DeletePolicies, release.HookDeletePolicy(value))
})
}
@ -201,7 +201,7 @@ func hasAnyAnnotation(entry SimpleHead) bool {
//
// If no weight is found, the assigned weight is 0
func calculateHookWeight(entry SimpleHead) int {
hws := entry.Metadata.Annotations[hooks.HookWeightAnno]
hws := entry.Metadata.Annotations[release.HookWeightAnnotation]
hw, err := strconv.Atoi(hws)
if err != nil {
hw = 0

@ -116,7 +116,7 @@ metadata:
name: []string{"eighth", "example-test"},
path: "eight",
kind: []string{"ConfigMap", "Pod"},
hooks: map[string][]release.HookEvent{"eighth": nil, "example-test": {release.HookReleaseTestSuccess}},
hooks: map[string][]release.HookEvent{"eighth": nil, "example-test": {release.HookTest}},
manifest: `kind: ConfigMap
apiVersion: v1
metadata:
@ -129,7 +129,7 @@ kind: Pod
metadata:
name: example-test
annotations:
"helm.sh/hook": test-success
"helm.sh/hook": test
`,
},
}

@ -29,7 +29,7 @@ kind: Pod
metadata:
name: finding-nemo,
annotations:
"helm.sh/hook": test-success
"helm.sh/hook": test
spec:
containers:
- name: nemo-test
@ -42,7 +42,7 @@ kind: Pod
metadata:
name: finding-nemo,
annotations:
"helm.sh/hook": test-success
"helm.sh/hook": test
spec:
containers:
- name: nemo-test

Loading…
Cancel
Save