diff --git a/cmd/helm/release_testing.go b/cmd/helm/release_testing.go index 2637cbb9f..05438f0a7 100644 --- a/cmd/helm/release_testing.go +++ b/cmd/helm/release_testing.go @@ -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 { client := action.NewReleaseTesting(cfg) - var outfmt = output.Table + var outfmt output.Format var outputLogs bool var filter []string @@ -76,7 +76,10 @@ func newReleaseTestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command 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 fmt.Fprintln(out) 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.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)") + bindOutputFlag(cmd, &outfmt) return cmd } diff --git a/pkg/action/hooks.go b/pkg/action/hooks.go index 40c1ffdb6..3d0a0871a 100644 --- a/pkg/action/hooks.go +++ b/pkg/action/hooks.go @@ -17,10 +17,13 @@ package action import ( "bytes" + "context" "sort" "time" "github.com/pkg/errors" + v1 "k8s.io/api/core/v1" + "k8s.io/client-go/kubernetes" "helm.sh/helm/v3/pkg/release" 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 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 { // Set default delete policy to before-hook-creation 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) // Note the time of success/failure 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 if err != nil { h.LastRun.Phase = release.HookPhaseFailed @@ -107,6 +124,17 @@ func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent, 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 type hookByWeight []*release.Hook @@ -149,3 +177,13 @@ func hookHasDeletePolicy(h *release.Hook, policy release.HookDeletePolicy) bool } 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 +} diff --git a/pkg/release/hook.go b/pkg/release/hook.go index cb9955582..90e7b6f55 100644 --- a/pkg/release/hook.go +++ b/pkg/release/hook.go @@ -78,6 +78,9 @@ type Hook struct { 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. type HookExecution struct { // StartedAt indicates the date/time this hook was started @@ -86,6 +89,10 @@ type HookExecution struct { CompletedAt time.Time `json:"completed_at,omitempty"` // Phase indicates whether the hook completed successfully 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