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 // Install, base case
{ {
name: "basic install", name: "basic install",
cmd: "install aeneas testdata/testcharts/empty", cmd: "install aeneas testdata/testcharts/empty --namespace default",
golden: "output/install.txt", golden: "output/install.txt",
}, },

@ -16,16 +16,13 @@ limitations under the License.
package main package main
import ( import (
"fmt"
"io" "io"
"time" "time"
"github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/cmd/helm/require" "helm.sh/helm/cmd/helm/require"
"helm.sh/helm/pkg/action" "helm.sh/helm/pkg/action"
"helm.sh/helm/pkg/release"
) )
const releaseTestRunHelp = ` const releaseTestRunHelp = `
@ -44,27 +41,7 @@ func newReleaseTestRunCmd(cfg *action.Configuration, out io.Writer) *cobra.Comma
Long: releaseTestRunHelp, Long: releaseTestRunHelp,
Args: require.ExactArgs(1), Args: require.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
c, errc := client.Run(args[0]) return 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")
}
}
}, },
} }
@ -74,11 +51,3 @@ func newReleaseTestRunCmd(cfg *action.Configuration, out io.Writer) *cobra.Comma
return cmd 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) { 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() info.LastDeployed = time.Unix(1452902400, 0).UTC()
return []*release.Release{{ return []*release.Release{{
Name: "flummoxed-chickadee", Name: "flummoxed-chickadee",
Info: info, Namespace: "default",
Chart: &chart.Chart{}, 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", name: "get status of a deployed release with test suite",
cmd: "status flummoxed-chickadee", cmd: "status flummoxed-chickadee",
golden: "output/status-with-test-suite.txt", golden: "output/status-with-test-suite.txt",
rels: releasesMockWithStatus(&release.Info{ rels: releasesMockWithStatus(
Status: release.StatusDeployed, &release.Info{
LastTestSuiteRun: &release.TestSuite{ Status: release.StatusDeployed,
Results: []*release.TestRun{{
Name: "test run 1",
Status: release.TestRunSuccess,
Info: "extra info",
}, {
Name: "test run 2",
Status: release.TestRunFailure,
}},
}, },
}), &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) runTestCmd(t, tests)
} }
func mustParseTime(t string) time.Time {
res, _ := time.Parse(time.RFC3339, t)
return res
}

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

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

@ -1,12 +1,15 @@
NAME: flummoxed-chickadee NAME: flummoxed-chickadee
LAST DEPLOYED: 2016-01-16 00:00:00 +0000 UTC LAST DEPLOYED: 2016-01-16 00:00:00 +0000 UTC
NAMESPACE: NAMESPACE: default
STATUS: deployed STATUS: deployed
TEST SUITE: TEST SUITE: passing-test
Last Started: 0001-01-01 00:00:00 +0000 UTC Last Started: 2006-01-02 15:04:05 +0000 UTC
Last Completed: 0001-01-01 00:00:00 +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 NAME: flummoxed-chickadee
LAST DEPLOYED: 2016-01-16 00:00:00 +0000 UTC LAST DEPLOYED: 2016-01-16 00:00:00 +0000 UTC
NAMESPACE: NAMESPACE: default
STATUS: deployed STATUS: deployed

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

@ -17,11 +17,8 @@ limitations under the License.
package action package action
import ( import (
"bytes"
"path" "path"
"regexp" "regexp"
"sort"
"strings"
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -32,7 +29,6 @@ import (
"helm.sh/helm/internal/experimental/registry" "helm.sh/helm/internal/experimental/registry"
"helm.sh/helm/pkg/chartutil" "helm.sh/helm/pkg/chartutil"
"helm.sh/helm/pkg/hooks"
"helm.sh/helm/pkg/kube" "helm.sh/helm/pkg/kube"
"helm.sh/helm/pkg/release" "helm.sh/helm/pkg/release"
"helm.sh/helm/pkg/storage" "helm.sh/helm/pkg/storage"
@ -204,75 +200,3 @@ type RESTClientGetter interface {
ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error)
ToRESTMapper() (meta.RESTMapper, 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: metadata:
name: finding-nemo, name: finding-nemo,
annotations: annotations:
"helm.sh/hook": test-success "helm.sh/hook": test
spec: spec:
containers: containers:
- name: nemo-test - name: nemo-test
@ -231,7 +231,7 @@ func namedReleaseStub(name string, status release.Status) *release.Release {
Path: "finding-nemo", Path: "finding-nemo",
Manifest: manifestWithTestHook, Manifest: manifestWithTestHook,
Events: []release.HookEvent{ 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/engine"
"helm.sh/helm/pkg/getter" "helm.sh/helm/pkg/getter"
"helm.sh/helm/pkg/helmpath" "helm.sh/helm/pkg/helmpath"
"helm.sh/helm/pkg/hooks"
kubefake "helm.sh/helm/pkg/kube/fake" kubefake "helm.sh/helm/pkg/kube/fake"
"helm.sh/helm/pkg/release" "helm.sh/helm/pkg/release"
"helm.sh/helm/pkg/releaseutil" "helm.sh/helm/pkg/releaseutil"
@ -169,7 +168,7 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release.
return rel, nil 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 i.Replace {
if err := i.replaceRelease(rel); err != nil { if err := i.replaceRelease(rel); err != nil {
return nil, err return nil, err
@ -187,7 +186,7 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release.
// pre-install hooks // pre-install hooks
if !i.DisableHooks { 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)) 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 !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)) 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) 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. // NameAndChart returns the name and chart that should be used.
// //
// This will read the flags and handle name generation if necessary. // 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) _, err = instAction.cfg.Releases.Get(res.Name, res.Version)
is.Error(err) is.Error(err)
is.Len(res.Hooks, 1) 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") is.Equal(res.Info.Description, "Dry run complete")
} }
@ -194,7 +194,7 @@ func TestInstallRelease_NoHooks(t *testing.T) {
t.Fatalf("Failed install: %s", err) 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) { func TestInstallRelease_FailedHooks(t *testing.T) {
@ -209,7 +209,7 @@ func TestInstallRelease_FailedHooks(t *testing.T) {
res, err := instAction.Run(buildChart(), vals) res, err := instAction.Run(buildChart(), vals)
is.Error(err) is.Error(err)
is.Contains(res.Info.Description, "failed post-install") 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) { func TestInstallRelease_ReplaceRelease(t *testing.T) {

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

@ -22,7 +22,6 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"helm.sh/helm/pkg/release" "helm.sh/helm/pkg/release"
reltesting "helm.sh/helm/pkg/releasetesting"
) )
// ReleaseTesting is the action for testing a release. // 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. // Run executes 'helm test' against the given release.
func (r *ReleaseTesting) Run(name string) (<-chan *release.TestReleaseResponse, <-chan error) { func (r *ReleaseTesting) Run(name string) error {
errc := make(chan error, 1)
if err := validateReleaseName(name); err != nil { if err := validateReleaseName(name); err != nil {
errc <- errors.Errorf("releaseTest: Release name is invalid: %s", name) return errors.Errorf("releaseTest: Release name is invalid: %s", name)
return nil, errc
} }
// finds the non-deleted release with the given name // finds the non-deleted release with the given name
rel, err := r.cfg.Releases.Last(name) rel, err := r.cfg.Releases.Last(name)
if err != nil { if err != nil {
errc <- err return err
return nil, errc
} }
ch := make(chan *release.TestReleaseResponse, 1) if err := r.cfg.execHook(rel, release.HookTest, r.Timeout); err != nil {
testEnv := &reltesting.Environment{ r.cfg.Releases.Update(rel)
Namespace: rel.Namespace, return err
KubeClient: r.cfg.KubeClient,
Timeout: r.Timeout,
Messages: ch,
} }
r.cfg.Log("running tests for release %s", rel.Name)
tSuite := reltesting.NewTestSuite(rel)
go func() { return r.cfg.Releases.Update(rel)
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
} }

@ -23,7 +23,6 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"helm.sh/helm/pkg/hooks"
"helm.sh/helm/pkg/release" "helm.sh/helm/pkg/release"
) )
@ -147,7 +146,7 @@ func (r *Rollback) performRollback(currentRelease, targetRelease *release.Releas
// pre-rollback hooks // pre-rollback hooks
if !r.DisableHooks { 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 return targetRelease, err
} }
} else { } else {
@ -188,7 +187,7 @@ func (r *Rollback) performRollback(currentRelease, targetRelease *release.Releas
// post-rollback hooks // post-rollback hooks
if !r.DisableHooks { 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 return targetRelease, err
} }
} }

@ -22,7 +22,6 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"helm.sh/helm/pkg/hooks"
"helm.sh/helm/pkg/release" "helm.sh/helm/pkg/release"
"helm.sh/helm/pkg/releaseutil" "helm.sh/helm/pkg/releaseutil"
) )
@ -91,7 +90,7 @@ func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error)
res := &release.UninstallReleaseResponse{Release: rel} res := &release.UninstallReleaseResponse{Release: rel}
if !u.DisableHooks { 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 return res, err
} }
} else { } else {
@ -108,7 +107,7 @@ func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error)
res.Info = kept res.Info = kept
if !u.DisableHooks { 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) errs = append(errs, err)
} }
} }
@ -150,6 +149,14 @@ func (u *Uninstall) purgeReleases(rels ...*release.Release) error {
return nil 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 // deleteRelease deletes the release and returns manifests that were kept in the deletion process
func (u *Uninstall) deleteRelease(rel *release.Release) (string, []error) { func (u *Uninstall) deleteRelease(rel *release.Release) (string, []error) {
caps, err := u.cfg.getCapabilities() caps, err := u.cfg.getCapabilities()

@ -26,7 +26,6 @@ import (
"helm.sh/helm/pkg/chart" "helm.sh/helm/pkg/chart"
"helm.sh/helm/pkg/chartutil" "helm.sh/helm/pkg/chartutil"
"helm.sh/helm/pkg/hooks"
"helm.sh/helm/pkg/kube" "helm.sh/helm/pkg/kube"
"helm.sh/helm/pkg/release" "helm.sh/helm/pkg/release"
"helm.sh/helm/pkg/releaseutil" "helm.sh/helm/pkg/releaseutil"
@ -210,7 +209,7 @@ func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Relea
// pre-upgrade hooks // pre-upgrade hooks
if !u.DisableHooks { 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)) return u.failRelease(upgradedRelease, fmt.Errorf("pre-upgrade hooks failed: %s", err))
} }
} else { } else {
@ -242,7 +241,7 @@ func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Relea
// post-upgrade hooks // post-upgrade hooks
if !u.DisableHooks { 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)) 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 // - Jobs: A job is marked "Ready" when it has successfully completed. This is
// ascertained by watching the Status fields in a job's output. // 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. // Handling for other kinds will be added as necessary.
func (c *Client) WatchUntilReady(resources ResourceList, timeout time.Duration) error { 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 // the status go into a good state. For other types, like ReplicaSet
// we don't really do anything to support these as hooks. // we don't really do anything to support these as hooks.
c.Log("Add/Modify event for %s: %v", info.Name, e.Type) 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) return c.waitForJob(obj, info.Name)
case "Pod":
return c.waitForPodSuccess(obj, info.Name)
} }
return true, nil return true, nil
case watch.Deleted: case watch.Deleted:
@ -432,6 +437,30 @@ func (c *Client) waitForJob(obj runtime.Object, name string) (bool, error) {
return false, nil 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. // scrubValidationError removes kubectl info from the message.
func scrubValidationError(err error) error { func scrubValidationError(err error) error {
if err == nil { if err == nil {

@ -23,7 +23,7 @@ import (
v1 "k8s.io/api/core/v1" 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. // A KubernetesClient must be concurrency safe.
type Interface interface { type Interface interface {
@ -37,7 +37,8 @@ type Interface interface {
// Watch the resource in reader until it is "ready". This method // 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 // For all other kinds, it means the kind was created or modified without
// error. // error.
WatchUntilReady(resources ResourceList, timeout time.Duration) error WatchUntilReady(resources ResourceList, timeout time.Duration) error

@ -1,10 +1,11 @@
/* /*
Copyright The Helm Authors. Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at 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 Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
@ -15,23 +16,24 @@ limitations under the License.
package release package release
import "time" import (
"time"
)
// HookEvent specifies the hook event // HookEvent specifies the hook event
type HookEvent string type HookEvent string
// Hook event types // Hook event types
const ( const (
HookPreInstall HookEvent = "pre-install" HookPreInstall HookEvent = "pre-install"
HookPostInstall HookEvent = "post-install" HookPostInstall HookEvent = "post-install"
HookPreDelete HookEvent = "pre-delete" HookPreDelete HookEvent = "pre-delete"
HookPostDelete HookEvent = "post-delete" HookPostDelete HookEvent = "post-delete"
HookPreUpgrade HookEvent = "pre-upgrade" HookPreUpgrade HookEvent = "pre-upgrade"
HookPostUpgrade HookEvent = "post-upgrade" HookPostUpgrade HookEvent = "post-upgrade"
HookPreRollback HookEvent = "pre-rollback" HookPreRollback HookEvent = "pre-rollback"
HookPostRollback HookEvent = "post-rollback" HookPostRollback HookEvent = "post-rollback"
HookReleaseTestSuccess HookEvent = "release-test-success" HookTest HookEvent = "test"
HookReleaseTestFailure HookEvent = "release-test-failure"
) )
func (x HookEvent) String() string { return string(x) } func (x HookEvent) String() string { return string(x) }
@ -41,13 +43,22 @@ type HookDeletePolicy string
// Hook delete policy types // Hook delete policy types
const ( const (
HookSucceeded HookDeletePolicy = "succeeded" HookSucceeded HookDeletePolicy = "hook-succeeded"
HookFailed HookDeletePolicy = "failed" HookFailed HookDeletePolicy = "hook-failed"
HookBeforeHookCreation HookDeletePolicy = "before-hook-creation" HookBeforeHookCreation HookDeletePolicy = "before-hook-creation"
) )
func (x HookDeletePolicy) String() string { return string(x) } 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. // Hook defines a hook object.
type Hook struct { type Hook struct {
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
@ -60,9 +71,36 @@ type Hook struct {
// Events are the events that this hook fires on. // Events are the events that this hook fires on.
Events []HookEvent `json:"events,omitempty"` Events []HookEvent `json:"events,omitempty"`
// LastRun indicates the date/time this was last run. // 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 indicates the sort order for execution among similar Hook type
Weight int `json:"weight,omitempty"` Weight int `json:"weight,omitempty"`
// DeletePolicies are the policies that indicate when to delete the hook // DeletePolicies are the policies that indicate when to delete the hook
DeletePolicies []HookDeletePolicy `json:"delete_policies,omitempty"` 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"` Resources string `json:"resources,omitempty"`
// Contains the rendered templates/NOTES.txt if available // Contains the rendered templates/NOTES.txt if available
Notes string `json:"notes,omitempty"` 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. // MockReleaseOptions allows for user-configurable options on mock release objects.
type MockReleaseOptions struct { type MockReleaseOptions struct {
Name string Name string
Version int Version int
Chart *chart.Chart Chart *chart.Chart
Status Status Status Status
Namespace string Namespace string
TestSuiteResults []*TestRun
} }
// Mock creates a mock release object based on options set by MockReleaseOptions. This function should typically not be used outside of testing. // 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", Description: "Release mock",
} }
if len(opts.TestSuiteResults) > 0 {
info.LastTestSuiteRun = &TestSuite{
StartedAt: date,
CompletedAt: date,
Results: opts.TestSuiteResults,
}
}
return &Release{ return &Release{
Name: name, Name: name,
Info: info, Info: info,
@ -114,7 +105,7 @@ func Mock(opts *MockReleaseOptions) *Release {
Kind: "Job", Kind: "Job",
Path: "pre-install-hook.yaml", Path: "pre-install-hook.yaml",
Manifest: MockHookTemplate, Manifest: MockHookTemplate,
LastRun: date, LastRun: HookExecution{},
Events: []HookEvent{HookPreInstall}, Events: []HookEvent{HookPreInstall},
}, },
}, },

@ -32,9 +32,3 @@ type UninstallReleaseResponse struct {
// Info is an uninstall message // Info is an uninstall message
Info string `json:"info,omitempty"` 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" "sigs.k8s.io/yaml"
"helm.sh/helm/pkg/chartutil" "helm.sh/helm/pkg/chartutil"
"helm.sh/helm/pkg/hooks"
"helm.sh/helm/pkg/release" "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. // TODO: Refactor this out. It's here because naming conventions were not followed through.
// So fix the Test hook names and then remove this. // So fix the Test hook names and then remove this.
var events = map[string]release.HookEvent{ var events = map[string]release.HookEvent{
hooks.PreInstall: release.HookPreInstall, release.HookPreInstall.String(): release.HookPreInstall,
hooks.PostInstall: release.HookPostInstall, release.HookPostInstall.String(): release.HookPostInstall,
hooks.PreDelete: release.HookPreDelete, release.HookPreDelete.String(): release.HookPreDelete,
hooks.PostDelete: release.HookPostDelete, release.HookPostDelete.String(): release.HookPostDelete,
hooks.PreUpgrade: release.HookPreUpgrade, release.HookPreUpgrade.String(): release.HookPreUpgrade,
hooks.PostUpgrade: release.HookPostUpgrade, release.HookPostUpgrade.String(): release.HookPostUpgrade,
hooks.PreRollback: release.HookPreRollback, release.HookPreRollback.String(): release.HookPreRollback,
hooks.PostRollback: release.HookPostRollback, release.HookPostRollback.String(): release.HookPostRollback,
hooks.ReleaseTestSuccess: release.HookReleaseTestSuccess, release.HookTest.String(): release.HookTest,
hooks.ReleaseTestFailure: release.HookReleaseTestFailure, // 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 // SortManifests takes a map of filename/YAML contents, splits the file
@ -142,7 +142,7 @@ func (file *manifestFile) sort(result *result) error {
continue continue
} }
hookTypes, ok := entry.Metadata.Annotations[hooks.HookAnno] hookTypes, ok := entry.Metadata.Annotations[release.HookAnnotation]
if !ok { if !ok {
result.generic = append(result.generic, Manifest{ result.generic = append(result.generic, Manifest{
Name: file.path, Name: file.path,
@ -182,7 +182,7 @@ func (file *manifestFile) sort(result *result) error {
result.hooks = append(result.hooks, h) 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)) 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 // If no weight is found, the assigned weight is 0
func calculateHookWeight(entry SimpleHead) int { func calculateHookWeight(entry SimpleHead) int {
hws := entry.Metadata.Annotations[hooks.HookWeightAnno] hws := entry.Metadata.Annotations[release.HookWeightAnnotation]
hw, err := strconv.Atoi(hws) hw, err := strconv.Atoi(hws)
if err != nil { if err != nil {
hw = 0 hw = 0

@ -116,7 +116,7 @@ metadata:
name: []string{"eighth", "example-test"}, name: []string{"eighth", "example-test"},
path: "eight", path: "eight",
kind: []string{"ConfigMap", "Pod"}, 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 manifest: `kind: ConfigMap
apiVersion: v1 apiVersion: v1
metadata: metadata:
@ -129,7 +129,7 @@ kind: Pod
metadata: metadata:
name: example-test name: example-test
annotations: annotations:
"helm.sh/hook": test-success "helm.sh/hook": test
`, `,
}, },
} }

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

Loading…
Cancel
Save