Add JSON/YAML output support for `helm test`

Co-authored-by: Simon Alling <alling.simon@gmail.com>

Signed-off-by: Andreas Lindhé <andreas@lindhe.io>
pull/9677/head
Andreas Lindhé 4 years ago
parent 43853ea772
commit 4cb207b425

@ -39,7 +39,7 @@ The tests to be run are defined in the chart that was installed.
func newReleaseTestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { func newReleaseTestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
client := action.NewReleaseTesting(cfg) client := action.NewReleaseTesting(cfg)
var outfmt = output.Table var outfmt output.Format
var outputLogs bool var outputLogs bool
var filter []string var filter []string
@ -76,7 +76,10 @@ func newReleaseTestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command
return err return err
} }
if outputLogs { // The logs are always included when JSON or YAML output is used.
// With table output, we print logs if and only if explicitly requested,
// to preserve backwards compatibility.
if outfmt == output.Table && outputLogs {
// Print a newline to stdout to separate the output // Print a newline to stdout to separate the output
fmt.Fprintln(out) fmt.Fprintln(out)
if err := client.GetPodLogs(out, rel); err != nil { if err := client.GetPodLogs(out, rel); err != nil {
@ -90,8 +93,9 @@ func newReleaseTestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command
f := cmd.Flags() f := cmd.Flags()
f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)") f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)")
f.BoolVar(&outputLogs, "logs", false, "dump the logs from test pods (this runs after all tests are complete, but before any cleanup)") f.BoolVar(&outputLogs, "logs", false, "dump the logs from test pods even with table output (this runs after all tests are complete, but before any cleanup)")
f.StringSliceVar(&filter, "filter", []string{}, "specify tests by attribute (currently \"name\") using attribute=value syntax or '!attribute=value' to exclude a test (can specify multiple or separate values with commas: name=test1,name=test2)") f.StringSliceVar(&filter, "filter", []string{}, "specify tests by attribute (currently \"name\") using attribute=value syntax or '!attribute=value' to exclude a test (can specify multiple or separate values with commas: name=test1,name=test2)")
bindOutputFlag(cmd, &outfmt)
return cmd return cmd
} }

@ -17,10 +17,13 @@ package action
import ( import (
"bytes" "bytes"
"context"
"sort" "sort"
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
v1 "k8s.io/api/core/v1"
"k8s.io/client-go/kubernetes"
"helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/release"
helmtime "helm.sh/helm/v3/pkg/time" helmtime "helm.sh/helm/v3/pkg/time"
@ -41,6 +44,11 @@ func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent,
// hooke are pre-ordered by kind, so keep order stable // hooke are pre-ordered by kind, so keep order stable
sort.Stable(hookByWeight(executingHooks)) sort.Stable(hookByWeight(executingHooks))
client, err := cfg.KubernetesClientSet()
if err != nil {
return errors.Wrapf(err, "unable to create Kubernetes client set to fetch pod logs")
}
for _, h := range executingHooks { for _, h := range executingHooks {
// Set default delete policy to before-hook-creation // Set default delete policy to before-hook-creation
if h.DeletePolicies == nil || len(h.DeletePolicies) == 0 { if h.DeletePolicies == nil || len(h.DeletePolicies) == 0 {
@ -83,6 +91,15 @@ func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent,
err = cfg.KubeClient.WatchUntilReady(resources, timeout) err = cfg.KubeClient.WatchUntilReady(resources, timeout)
// Note the time of success/failure // Note the time of success/failure
h.LastRun.CompletedAt = helmtime.Now() h.LastRun.CompletedAt = helmtime.Now()
if isTestHook(h) {
hookLog, err := getHookLog(client, rl, h)
if err != nil {
return err
}
h.LastRun.Log = &hookLog
}
// Mark hook as succeeded or failed // Mark hook as succeeded or failed
if err != nil { if err != nil {
h.LastRun.Phase = release.HookPhaseFailed h.LastRun.Phase = release.HookPhaseFailed
@ -107,6 +124,17 @@ func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent,
return nil return nil
} }
// getHookLog gets the log from the pod associated with the given hook, which is expected to be a test hook.
func getHookLog(client kubernetes.Interface, rel *release.Release, hook *release.Hook) (release.HookLog, error) {
req := client.CoreV1().Pods(rel.Namespace).GetLogs(hook.Name, &v1.PodLogOptions{})
responseBody, err := req.DoRaw(context.Background())
if err != nil {
var nothing release.HookLog
return nothing, errors.Wrapf(err, "unable to get pod logs for %s", hook.Name)
}
return release.HookLog(responseBody), nil
}
// hookByWeight is a sorter for hooks // hookByWeight is a sorter for hooks
type hookByWeight []*release.Hook type hookByWeight []*release.Hook
@ -149,3 +177,13 @@ func hookHasDeletePolicy(h *release.Hook, policy release.HookDeletePolicy) bool
} }
return false return false
} }
// isTestHook determines whether a hook is a test hook.
func isTestHook(h *release.Hook) bool {
for _, e := range h.Events {
if e == release.HookTest {
return true
}
}
return false
}

@ -78,6 +78,9 @@ type Hook struct {
DeletePolicies []HookDeletePolicy `json:"delete_policies,omitempty"` DeletePolicies []HookDeletePolicy `json:"delete_policies,omitempty"`
} }
// A HookLog represents a log associated with a hook.
type HookLog string
// A HookExecution records the result for the last execution of a hook for a given release. // A HookExecution records the result for the last execution of a hook for a given release.
type HookExecution struct { type HookExecution struct {
// StartedAt indicates the date/time this hook was started // StartedAt indicates the date/time this hook was started
@ -86,6 +89,10 @@ type HookExecution struct {
CompletedAt time.Time `json:"completed_at,omitempty"` CompletedAt time.Time `json:"completed_at,omitempty"`
// Phase indicates whether the hook completed successfully // Phase indicates whether the hook completed successfully
Phase HookPhase `json:"phase"` Phase HookPhase `json:"phase"`
// Log holds the log associated with the hook.
// The empty string represents the output from a pod that didn't print anything.
// The nil pointer represents the absence of a log (for example if the pod couldn't be created).
Log *HookLog `json:"log,omitempty"` // We need to distinguish the empty log from no log; otherwise both would be rendered identically in JSON/YAML.
} }
// A HookPhase indicates the state of a hook execution // A HookPhase indicates the state of a hook execution

Loading…
Cancel
Save