feat(test): define tests as Jobs and allow arbitrary supporting resources

This updates commands install, upgrade, delete, and test to share the
same implementation for hook execution.

BREAKING CHANGES:
- The `test-failure` hook annotation is removed.

Signed-off-by: Jacob LeGrone <git@jacob.work>
pull/6054/head
Jacob LeGrone 6 years ago
parent 533a369464
commit 72127c391c
No known key found for this signature in database
GPG Key ID: 5FD0852F235368C1

@ -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)
}

@ -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,106 @@
/*
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(hs []*release.Hook, hook release.HookEvent, timeout time.Duration) error {
executingHooks := []*release.Hook{}
for _, h := range hs {
for _, e := range h.Events {
if e == hook {
executingHooks = append(executingHooks, h)
}
}
}
sort.Sort(hookByWeight(executingHooks))
for _, h := range executingHooks {
if err := deleteHookByPolicy(cfg, h, release.HookBeforeHookCreation); err != nil {
return err
}
b := bytes.NewBufferString(h.Manifest)
if err := cfg.KubeClient.Create(b); err != nil {
return errors.Wrapf(err, "warning: Hook %s %s failed", hook, h.Path)
}
b.Reset()
b.WriteString(h.Manifest)
if err := cfg.KubeClient.WatchUntilReady(b, 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(cfg, h, release.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(cfg, h, release.HookSucceeded); err != nil {
return err
}
h.LastRun = time.Now()
}
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 deleteHookByPolicy(cfg *Configuration, h *release.Hook, policy release.HookDeletePolicy) error {
if hookHasDeletePolicy(h, policy) {
b := bytes.NewBufferString(h.Manifest)
return cfg.KubeClient.Delete(b)
}
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
}

@ -25,7 +25,6 @@ import (
"os"
"path"
"path/filepath"
"sort"
"strings"
"text/template"
"time"
@ -40,7 +39,6 @@ import (
"helm.sh/helm/pkg/downloader"
"helm.sh/helm/pkg/engine"
"helm.sh/helm/pkg/getter"
"helm.sh/helm/pkg/hooks"
kubefake "helm.sh/helm/pkg/kube/fake"
"helm.sh/helm/pkg/release"
"helm.sh/helm/pkg/releaseutil"
@ -198,7 +196,7 @@ func (i *Install) Run(chrt *chart.Chart) (*release.Release, error) {
// pre-install hooks
if !i.DisableHooks {
if err := i.execHook(rel.Hooks, hooks.PreInstall); err != nil {
if err := i.execHook(rel.Hooks, release.HookPreInstall); err != nil {
return i.failRelease(rel, fmt.Errorf("failed pre-install: %s", err))
}
}
@ -220,7 +218,7 @@ func (i *Install) Run(chrt *chart.Chart) (*release.Release, error) {
}
if !i.DisableHooks {
if err := i.execHook(rel.Hooks, hooks.PostInstall); err != nil {
if err := i.execHook(rel.Hooks, release.HookPostInstall); err != nil {
return i.failRelease(rel, fmt.Errorf("failed post-install: %s", err))
}
}
@ -466,86 +464,8 @@ func (i *Install) validateManifest(manifest io.Reader) error {
}
// execHook executes all of the hooks for the given hook event.
func (i *Install) execHook(hs []*release.Hook, hook string) 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(i.cfg, h, hooks.BeforeHookCreation); err != nil {
return err
}
b := bytes.NewBufferString(h.Manifest)
if err := i.cfg.KubeClient.Create(b); err != nil {
return errors.Wrapf(err, "warning: Release %s %s %s failed", i.ReleaseName, hook, h.Path)
}
b.Reset()
b.WriteString(h.Manifest)
if err := i.cfg.KubeClient.WatchUntilReady(b, i.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(i.cfg, 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(i.cfg, h, hooks.HookSucceeded); err != nil {
return err
}
h.LastRun = time.Now()
}
return nil
}
// 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
func (i *Install) execHook(hs []*release.Hook, hook release.HookEvent) error {
return i.cfg.execHook(hs, hook, i.Timeout)
}
// NameAndChart returns the name and chart that should be used.

@ -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,16 @@ 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,
}
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.execHook(rel.Hooks, release.HookTest, r.Timeout)
}

@ -19,12 +19,10 @@ package action
import (
"bytes"
"fmt"
"sort"
"time"
"github.com/pkg/errors"
"helm.sh/helm/pkg/hooks"
"helm.sh/helm/pkg/release"
)
@ -140,7 +138,7 @@ func (r *Rollback) performRollback(currentRelease, targetRelease *release.Releas
// pre-rollback hooks
if !r.DisableHooks {
if err := r.execHook(targetRelease.Hooks, hooks.PreRollback); err != nil {
if err := r.execHook(targetRelease.Hooks, release.HookPreRollback); err != nil {
return targetRelease, err
}
} else {
@ -173,7 +171,7 @@ func (r *Rollback) performRollback(currentRelease, targetRelease *release.Releas
// post-rollback hooks
if !r.DisableHooks {
if err := r.execHook(targetRelease.Hooks, hooks.PostRollback); err != nil {
if err := r.execHook(targetRelease.Hooks, release.HookPostRollback); err != nil {
return targetRelease, err
}
}
@ -195,59 +193,6 @@ func (r *Rollback) performRollback(currentRelease, targetRelease *release.Releas
}
// execHook executes all of the hooks for the given hook event.
func (r *Rollback) execHook(hs []*release.Hook, hook string) error {
timeout := r.Timeout
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(r.cfg, h, hooks.BeforeHookCreation); err != nil {
return err
}
b := bytes.NewBufferString(h.Manifest)
if err := r.cfg.KubeClient.Create(b); err != nil {
return errors.Wrapf(err, "warning: Hook %s %s failed", hook, h.Path)
}
b.Reset()
b.WriteString(h.Manifest)
if err := r.cfg.KubeClient.WatchUntilReady(b, 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(r.cfg, 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(r.cfg, 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(cfg *Configuration, h *release.Hook, policy string) error {
if hookHasDeletePolicy(h, policy) {
b := bytes.NewBufferString(h.Manifest)
return cfg.KubeClient.Delete(b)
}
return nil
func (r *Rollback) execHook(hs []*release.Hook, hook release.HookEvent) error {
return r.cfg.execHook(hs, hook, r.Timeout)
}

@ -18,13 +18,11 @@ package action
import (
"bytes"
"sort"
"strings"
"time"
"github.com/pkg/errors"
"helm.sh/helm/pkg/hooks"
"helm.sh/helm/pkg/kube"
"helm.sh/helm/pkg/release"
"helm.sh/helm/pkg/releaseutil"
@ -94,7 +92,7 @@ func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error)
res := &release.UninstallReleaseResponse{Release: rel}
if !u.DisableHooks {
if err := u.execHook(rel.Hooks, hooks.PreDelete); err != nil {
if err := u.execHook(rel.Hooks, release.HookPreDelete); err != nil {
return res, err
}
} else {
@ -111,7 +109,7 @@ func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error)
res.Info = kept
if !u.DisableHooks {
if err := u.execHook(rel.Hooks, hooks.PostDelete); err != nil {
if err := u.execHook(rel.Hooks, release.HookPostDelete); err != nil {
errs = append(errs, err)
}
}
@ -162,51 +160,8 @@ func joinErrors(errs []error) string {
}
// execHook executes all of the hooks for the given hook event.
func (u *Uninstall) execHook(hs []*release.Hook, hook string) 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(u.cfg, h, hooks.BeforeHookCreation); err != nil {
return err
}
b := bytes.NewBufferString(h.Manifest)
if err := u.cfg.KubeClient.Create(b); err != nil {
return errors.Wrapf(err, "warning: Hook %s %s failed", hook, h.Path)
}
b.Reset()
b.WriteString(h.Manifest)
if err := u.cfg.KubeClient.WatchUntilReady(b, u.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(u.cfg, 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(u.cfg, h, hooks.HookSucceeded); err != nil {
return err
}
h.LastRun = time.Now()
}
return nil
func (u *Uninstall) execHook(hs []*release.Hook, hook release.HookEvent) error {
return u.cfg.execHook(hs, hook, u.Timeout)
}
// deleteRelease deletes the release and returns manifests that were kept in the deletion process

@ -19,14 +19,12 @@ package action
import (
"bytes"
"fmt"
"sort"
"time"
"github.com/pkg/errors"
"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"
@ -201,7 +199,7 @@ func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Relea
// pre-upgrade hooks
if !u.DisableHooks {
if err := u.execHook(upgradedRelease.Hooks, hooks.PreUpgrade); err != nil {
if err := u.execHook(upgradedRelease.Hooks, release.HookPreUpgrade); err != nil {
return u.failRelease(upgradedRelease, fmt.Errorf("pre-upgrade hooks failed: %s", err))
}
} else {
@ -222,7 +220,7 @@ func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Relea
// post-upgrade hooks
if !u.DisableHooks {
if err := u.execHook(upgradedRelease.Hooks, hooks.PostUpgrade); err != nil {
if err := u.execHook(upgradedRelease.Hooks, release.HookPostUpgrade); err != nil {
return u.failRelease(upgradedRelease, fmt.Errorf("post-upgrade hooks failed: %s", err))
}
}
@ -335,49 +333,6 @@ func validateManifest(c kube.Interface, manifest []byte) error {
}
// execHook executes all of the hooks for the given hook event.
func (u *Upgrade) execHook(hs []*release.Hook, hook string) error {
timeout := u.Timeout
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(u.cfg, h, hooks.BeforeHookCreation); err != nil {
return err
}
b := bytes.NewBufferString(h.Manifest)
if err := u.cfg.KubeClient.Create(b); err != nil {
return errors.Wrapf(err, "warning: Hook %s %s failed", hook, h.Path)
}
b.Reset()
b.WriteString(h.Manifest)
if err := u.cfg.KubeClient.WatchUntilReady(b, 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(u.cfg, 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(u.cfg, h, hooks.HookSucceeded); err != nil {
return err
}
h.LastRun = time.Now()
}
return nil
func (u *Upgrade) execHook(hs []*release.Hook, hook release.HookEvent) error {
return u.cfg.execHook(hs, hook, u.Timeout)
}

@ -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
}

@ -19,7 +19,6 @@ package kube // import "helm.sh/helm/pkg/kube"
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"strings"
@ -487,29 +486,3 @@ func scrubValidationError(err error) error {
}
return err
}
// WaitAndGetCompletedPodPhase waits up to a timeout until a pod enters a completed phase
// and returns said phase (PodSucceeded or PodFailed qualify).
func (c *Client) WaitAndGetCompletedPodPhase(name string, timeout time.Duration) (v1.PodPhase, error) {
client, _ := c.KubernetesClientSet()
to := int64(timeout)
watcher, err := client.CoreV1().Pods(c.namespace()).Watch(metav1.ListOptions{
FieldSelector: fmt.Sprintf("metadata.name=%s", name),
TimeoutSeconds: &to,
})
for event := range watcher.ResultChan() {
p, ok := event.Object.(*v1.Pod)
if !ok {
return v1.PodUnknown, fmt.Errorf("%s not a pod", name)
}
switch p.Status.Phase {
case v1.PodFailed:
return v1.PodFailed, nil
case v1.PodSucceeded:
return v1.PodSucceeded, nil
}
}
return v1.PodUnknown, err
}

@ -21,7 +21,6 @@ import (
"io"
"time"
v1 "k8s.io/api/core/v1"
"k8s.io/cli-runtime/pkg/resource"
"helm.sh/helm/pkg/kube"
@ -32,15 +31,14 @@ import (
// delegates all its calls to `PrintingKubeClient`
type FailingKubeClient struct {
PrintingKubeClient
CreateError error
WaitError error
GetError error
DeleteError error
WatchUntilReadyError error
UpdateError error
BuildError error
BuildUnstructuredError error
WaitAndGetCompletedPodPhaseError error
CreateError error
WaitError error
GetError error
}
// Create returns the configured error if set or prints
@ -106,11 +104,3 @@ func (f *FailingKubeClient) BuildUnstructured(r io.Reader) (kube.Result, error)
}
return f.PrintingKubeClient.Build(r)
}
// WaitAndGetCompletedPodPhase returns the configured error if set or prints
func (f *FailingKubeClient) WaitAndGetCompletedPodPhase(s string, d time.Duration) (v1.PodPhase, error) {
if f.WaitAndGetCompletedPodPhaseError != nil {
return v1.PodSucceeded, f.WaitAndGetCompletedPodPhaseError
}
return f.PrintingKubeClient.WaitAndGetCompletedPodPhase(s, d)
}

@ -20,7 +20,6 @@ import (
"io"
"time"
v1 "k8s.io/api/core/v1"
"k8s.io/cli-runtime/pkg/resource"
"helm.sh/helm/pkg/kube"
@ -77,8 +76,3 @@ func (p *PrintingKubeClient) Build(_ io.Reader) (kube.Result, error) {
func (p *PrintingKubeClient) BuildUnstructured(_ io.Reader) (kube.Result, error) {
return p.Build(nil)
}
// WaitAndGetCompletedPodPhase implements KubeClient WaitAndGetCompletedPodPhase.
func (p *PrintingKubeClient) WaitAndGetCompletedPodPhase(_ string, _ time.Duration) (v1.PodPhase, error) {
return v1.PodSucceeded, nil
}

@ -19,8 +19,6 @@ package kube
import (
"io"
"time"
v1 "k8s.io/api/core/v1"
)
// KubernetesClient represents a client capable of communicating with the Kubernetes API.
@ -57,10 +55,6 @@ type Interface interface {
Build(reader io.Reader) (Result, error)
BuildUnstructured(reader io.Reader) (Result, error)
// WaitAndGetCompletedPodPhase waits up to a timeout until a pod enters a completed phase
// and returns said phase (PodSucceeded or PodFailed qualify).
WaitAndGetCompletedPodPhase(name string, timeout time.Duration) (v1.PodPhase, error)
}
var _ Interface = (*Client)(nil)

@ -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,7 +16,9 @@ limitations under the License.
package release
import "time"
import (
"time"
)
// HookEvent specifies the hook event
type HookEvent string
@ -30,8 +33,7 @@ const (
HookPostUpgrade HookEvent = "post-upgrade"
HookPreRollback HookEvent = "pre-rollback"
HookPostRollback HookEvent = "post-rollback"
HookReleaseTestSuccess HookEvent = "release-test-success"
HookReleaseTestFailure HookEvent = "release-test-failure"
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"`

@ -1,120 +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 {
b := bytes.NewBufferString(test.manifest)
if err := env.KubeClient.Create(b); 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 {
err := env.KubeClient.Delete(bytes.NewBufferString(testManifest))
if err != nil {
env.streamError(err.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"
"testing"
"time"
v1 "k8s.io/api/core/v1"
"helm.sh/helm/pkg/kube"
"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{},
Timeout: 1,
Messages: make(chan *release.TestReleaseResponse, 1),
}
}
type mockKubeClient struct {
kube.Interface
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(_ io.Reader) error { return c.err }
func (c *mockKubeClient) Delete(_ io.Reader) error { return nil }

@ -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,16 @@ 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,
"test-success": release.HookTest,
}
// SortManifests takes a map of filename/YAML contents, splits the file
@ -142,7 +141,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 +181,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 +200,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