From 707604c9c01c14addee4ee1055f76ef869e27155 Mon Sep 17 00:00:00 2001 From: Charlie Getzen Date: Wed, 2 Oct 2024 12:51:39 -0700 Subject: [PATCH] feat: Add --log functionality for additional commands and resources Signed-off-by: Charlie Getzen --- cmd/helm/release_testing.go | 2 +- cmd/helm/upgrade.go | 28 ++++++++++++--- pkg/action/release_testing.go | 65 +++++++++++++++++++++++++++-------- pkg/action/upgrade.go | 26 ++++++++++++++ 4 files changed, 101 insertions(+), 20 deletions(-) diff --git a/cmd/helm/release_testing.go b/cmd/helm/release_testing.go index 136d785d1..3a5e2044b 100644 --- a/cmd/helm/release_testing.go +++ b/cmd/helm/release_testing.go @@ -90,7 +90,7 @@ 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 jobs or pods (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.BoolVar(&client.HideNotes, "hide-notes", false, "if set, do not show notes in test output. Does not affect presence in chart metadata") diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go index 108550cbf..77ec436ad 100644 --- a/cmd/helm/upgrade.go +++ b/cmd/helm/upgrade.go @@ -84,6 +84,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { valueOpts := &values.Options{} var outfmt output.Format var createNamespace bool + var outputLogs bool cmd := &cobra.Command{ Use: "upgrade [RELEASE] [CHART]", @@ -237,22 +238,39 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { cancel() }() - rel, err := client.RunWithContext(ctx, args[0], ch, vals) + rel, runErr := client.RunWithContext(ctx, args[0], ch, vals) - if err != nil { - return errors.Wrap(err, "UPGRADE FAILED") + // We only return an error if we weren't even able to get the + // release, otherwise we keep going so we can print status and logs + // if requested + if runErr != nil && rel == nil { + return errors.Wrap(runErr, "UPGRADE FAILED") } - if outfmt == output.Table { + if runErr == nil && outfmt == output.Table { fmt.Fprintf(out, "Release %q has been upgraded. Happy Helming!\n", args[0]) } - return outfmt.Write(out, &statusPrinter{rel, settings.Debug, false, false, false, client.HideNotes}) + if err := outfmt.Write(out, &statusPrinter{rel, settings.Debug, false, false, false, client.HideNotes}); err != nil { + return err + } + if outputLogs { + // Print a newline to stdout to separate the output + fmt.Fprintln(out) + if err := client.GetHookLogs(out, rel); err != nil { + return err + } + } + if runErr != nil { + return errors.Wrap(runErr, "UPGRADE FAILED") + } + return nil }, } f := cmd.Flags() f.BoolVar(&createNamespace, "create-namespace", false, "if --install is set, create the release namespace if not present") + f.BoolVar(&outputLogs, "logs", false, "dump the logs from hook jobs or pods (this runs after the upgrade completes)") f.BoolVarP(&client.Install, "install", "i", false, "if a release by this name doesn't already exist, run an install") f.BoolVar(&client.Devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored") f.StringVar(&client.DryRunOption, "dry-run", "", "simulate an install. If --dry-run is set with no option being specified or as '--dry-run=client', it will not attempt cluster connections. Setting '--dry-run=server' allows attempting cluster connections.") diff --git a/pkg/action/release_testing.go b/pkg/action/release_testing.go index aaffe47ca..b10d854cb 100644 --- a/pkg/action/release_testing.go +++ b/pkg/action/release_testing.go @@ -25,6 +25,9 @@ import ( "github.com/pkg/errors" v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/kubernetes" "helm.sh/helm/v3/pkg/chartutil" "helm.sh/helm/v3/pkg/release" @@ -113,10 +116,9 @@ func (r *ReleaseTesting) GetPodLogs(out io.Writer, rel *release.Release) error { if err != nil { return errors.Wrap(err, "unable to get kubernetes client to fetch pod logs") } - - hooksByWight := append([]*release.Hook{}, rel.Hooks...) - sort.Stable(hookByWeight(hooksByWight)) - for _, h := range hooksByWight { + hooksByWeight := append([]*release.Hook{}, rel.Hooks...) + sort.Stable(hookByWeight(hooksByWeight)) + for _, h := range hooksByWeight { for _, e := range h.Events { if e == release.HookTest { if contains(r.Filters[ExcludeNameFilter], h.Name) { @@ -125,20 +127,55 @@ func (r *ReleaseTesting) GetPodLogs(out io.Writer, rel *release.Release) error { if len(r.Filters[IncludeNameFilter]) > 0 && !contains(r.Filters[IncludeNameFilter], h.Name) { continue } - req := client.CoreV1().Pods(r.Namespace).GetLogs(h.Name, &v1.PodLogOptions{}) - logReader, err := req.Stream(context.Background()) - if err != nil { - return errors.Wrapf(err, "unable to get pod logs for %s", h.Name) + if err := getHookLogs(out, client, r.Namespace, h); err != nil { + return err } + } + } + } + return nil +} - fmt.Fprintf(out, "POD LOGS: %s\n", h.Name) - _, err = io.Copy(out, logReader) - fmt.Fprintln(out) - if err != nil { - return errors.Wrapf(err, "unable to write pod logs for %s", h.Name) - } +func getHookLogs(out io.Writer, client kubernetes.Interface, namespace string, h *release.Hook) error { + switch kind := h.Kind; kind { + case "Job": + job, err := client.BatchV1().Jobs(namespace).Get(context.Background(), h.Name, metav1.GetOptions{}) + if err != nil { + return errors.Wrapf(err, "unable to get job for %s", h.Name) + } + + pods, err := client.CoreV1().Pods(namespace).List(context.Background(), metav1.ListOptions{ + LabelSelector: labels.Set(job.Spec.Selector.MatchLabels).String(), + }) + if err != nil { + return errors.Wrapf(err, "failed to list pods for job %s", h.Name) + } + for _, pod := range pods.Items { + req := client.CoreV1().Pods(namespace).GetLogs(pod.Name, &v1.PodLogOptions{}) + logReader, err := req.Stream(context.Background()) + if err != nil { + return errors.Wrapf(err, "unable to get pod logs for %s", pod.Name) + } + fmt.Fprintf(out, "POD LOGS: %s\n", pod.Name) + _, err = io.Copy(out, logReader) + fmt.Fprintln(out) + if err != nil { + return errors.Wrapf(err, "unable to write pod logs for %s", pod.Name) } } + case "Pod": + req := client.CoreV1().Pods(namespace).GetLogs(h.Name, &v1.PodLogOptions{}) + logReader, err := req.Stream(context.Background()) + if err != nil { + return errors.Wrapf(err, "unable to get pod logs for %s", h.Name) + } + + fmt.Fprintf(out, "POD LOGS: %s\n", h.Name) + _, err = io.Copy(out, logReader) + fmt.Fprintln(out) + if err != nil { + return errors.Wrapf(err, "unable to write pod logs for %s", h.Name) + } } return nil } diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index a08d68495..56c6bc5fb 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -20,6 +20,8 @@ import ( "bytes" "context" "fmt" + "io" + "sort" "strings" "sync" "time" @@ -186,6 +188,30 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, chart *chart. return res, nil } +// GetPodLogs will write the logs for all test pods in the given release into +// the given writer. These can be immediately output to the user or captured for +// other uses +func (u *Upgrade) GetHookLogs(out io.Writer, rel *release.Release) error { + client, err := u.cfg.KubernetesClientSet() + if err != nil { + return errors.Wrap(err, "unable to get kubernetes client to fetch pod logs") + } + + hooksByWeight := append([]*release.Hook{}, rel.Hooks...) + sort.Stable(hookByWeight(hooksByWeight)) + + for _, h := range hooksByWeight { + for _, e := range h.Events { + if e != release.HookTest { + if err := getHookLogs(out, client, u.Namespace, h); err != nil { + return err + } + } + } + } + return nil +} + // isDryRun returns true if Upgrade is set to run as a DryRun func (u *Upgrade) isDryRun() bool { if u.DryRun || u.DryRunOption == "client" || u.DryRunOption == "server" || u.DryRunOption == "true" {