From 6d30fa59904f1936613e0882d3e3c2608351da0b Mon Sep 17 00:00:00 2001 From: Chris Berry Date: Fri, 21 Feb 2025 12:33:12 +0000 Subject: [PATCH] Add HookOutputFunc and generic yaml unmarshaller Signed-off-by: Chris Berry --- pkg/action/action.go | 13 ++++++++- pkg/action/hooks.go | 57 ++++++++++++++++++---------------------- pkg/kube/client.go | 4 +-- pkg/kube/client_test.go | 3 ++- pkg/kube/fake/printer.go | 2 +- pkg/kube/interface.go | 2 +- 6 files changed, 43 insertions(+), 38 deletions(-) diff --git a/pkg/action/action.go b/pkg/action/action.go index 8418a4c27..0d81a63cb 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -19,6 +19,8 @@ package action import ( "bytes" "fmt" + "io" + "log" "os" "path" "path/filepath" @@ -95,6 +97,9 @@ type Configuration struct { Capabilities *chartutil.Capabilities Log func(string, ...interface{}) + + // HookOutputFunc Called with container name and returns and expects writer that will receive the log output + HookOutputFunc func(namespace, pod, container string) io.Writer } // renderResources renders the templates in a chart @@ -122,7 +127,7 @@ func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Valu var err2 error // A `helm template` should not talk to the remote cluster. However, commands with the flag - //`--dry-run` with the value of `false`, `none`, or `server` should try to interact with the cluster. + // `--dry-run` with the value of `false`, `none`, or `server` should try to interact with the cluster. // It may break in interesting and exotic ways because other data (e.g. discovery) is mocked. if interactWithRemote && cfg.RESTClientGetter != nil { restConfig, err := cfg.RESTClientGetter.ToRESTConfig() @@ -422,6 +427,12 @@ func (cfg *Configuration) Init(getter genericclioptions.RESTClientGetter, namesp cfg.KubeClient = kc cfg.Releases = store cfg.Log = log + cfg.HookOutputFunc = defaultHookOutputWriter return nil } + +// defaultHookOutputWriter will write the Hook logs to log.Writer(). +func defaultHookOutputWriter(_, _, _ string) io.Writer { + return log.Writer() +} diff --git a/pkg/action/hooks.go b/pkg/action/hooks.go index dadcb27f6..b6c505807 100644 --- a/pkg/action/hooks.go +++ b/pkg/action/hooks.go @@ -19,16 +19,16 @@ import ( "bytes" "fmt" "log" + "slices" "sort" "time" "helm.sh/helm/v4/pkg/kube" - "helm.sh/helm/v4/pkg/chartutil" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/pkg/errors" + "gopkg.in/yaml.v3" "helm.sh/helm/v4/pkg/release" helmtime "helm.sh/helm/v4/pkg/time" @@ -179,22 +179,20 @@ func hookHasDeletePolicy(h *release.Hook, policy release.HookDeletePolicy) bool // outputLogsByPolicy outputs a pods logs if the hook policy instructs it to func (cfg *Configuration) outputLogsByPolicy(h *release.Hook, releaseNamespace string, policy release.HookOutputLogPolicy) error { if !hookHasOutputLogPolicy(h, policy) { - return nil + return nil } - namespace, err := cfg.deriveNamespace(h, releaseNamespace) - if err != nil { - return err - } - switch h.Kind { - case "Job": - return cfg.outputContainerLogsForListOptions(namespace, metav1.ListOptions{LabelSelector: fmt.Sprintf("job-name=%s", h.Name)}) - case "Pod": - return cfg.outputContainerLogsForListOptions(namespace, metav1.ListOptions{FieldSelector: fmt.Sprintf("metadata.name=%s", h.Name)}) - default: - return nil - } + namespace, err := cfg.deriveNamespace(h, releaseNamespace) + if err != nil { + return err + } + switch h.Kind { + case "Job": + return cfg.outputContainerLogsForListOptions(namespace, metav1.ListOptions{LabelSelector: fmt.Sprintf("job-name=%s", h.Name)}) + case "Pod": + return cfg.outputContainerLogsForListOptions(namespace, metav1.ListOptions{FieldSelector: fmt.Sprintf("metadata.name=%s", h.Name)}) + default: + return nil } - return nil } func (cfg *Configuration) outputContainerLogsForListOptions(namespace string, listOptions metav1.ListOptions) error { @@ -204,35 +202,30 @@ func (cfg *Configuration) outputContainerLogsForListOptions(namespace string, li if err != nil { return err } - err = kubeClient.OutputContainerLogsForPodList(podList, namespace, log.Writer()) + err = kubeClient.OutputContainerLogsForPodList(podList, namespace, cfg.HookOutputFunc) return err } return nil } func (cfg *Configuration) deriveNamespace(h *release.Hook, namespace string) (string, error) { - values, err := chartutil.ReadValues([]byte(h.Manifest)) + tmp := struct { + Metadata struct { + Namespace string + } + }{} + err := yaml.Unmarshal([]byte(h.Manifest), &tmp) if err != nil { - return "", errors.Wrapf(err, "unable to parse kubernetes manifest for output logs hook %s", h.Path) + return "", errors.Wrapf(err, "unable to parse metadata.namespace from kubernetes manifest for output logs hook %s", h.Path) } - value, err := values.PathValue("metadata.namespace") - switch err.(type) { - case nil: - return value.(string), nil - case chartutil.ErrNoValue: + if tmp.Metadata.Namespace == "" { return namespace, nil - default: - return "", errors.Wrapf(err, "unable to parse path of metadata.namespace in yaml for output logs hook %s", h.Path) } + return tmp.Metadata.Namespace, nil } // hookHasOutputLogPolicy determines whether the defined hook output log policy matches the hook output log policies // supported by helm. func hookHasOutputLogPolicy(h *release.Hook, policy release.HookOutputLogPolicy) bool { - for _, v := range h.OutputLogPolicies { - if policy == v { - return true - } - } - return false + return slices.Contains(h.OutputLogPolicies, policy) } diff --git a/pkg/kube/client.go b/pkg/kube/client.go index 361111fed..fd111c647 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -822,14 +822,14 @@ func (c *Client) GetPodList(namespace string, listOptions metav1.ListOptions) (* } // OutputContainerLogsForPodList is a helper that outputs logs for a list of pods -func (c *Client) OutputContainerLogsForPodList(podList *v1.PodList, namespace string, writer io.Writer) error { +func (c *Client) OutputContainerLogsForPodList(podList *v1.PodList, namespace string, writerFunc func(namespace, pod, container string) io.Writer) error { for _, pod := range podList.Items { for _, container := range pod.Spec.Containers { options := &v1.PodLogOptions{ Container: container.Name, } request := c.kubeClient.CoreV1().Pods(namespace).GetLogs(pod.Name, options) - err2 := copyRequestStreamToWriter(request, pod.Name, container.Name, writer) + err2 := copyRequestStreamToWriter(request, pod.Name, container.Name, writerFunc(namespace, pod.Name, container.Name)) if err2 != nil { return err2 } diff --git a/pkg/kube/client_test.go b/pkg/kube/client_test.go index d9bd72783..ff1335f0f 100644 --- a/pkg/kube/client_test.go +++ b/pkg/kube/client_test.go @@ -711,7 +711,8 @@ func TestOutputContainerLogsForPodList(t *testing.T) { kubeClient := k8sfake.NewSimpleClientset(&somePodList) c := Client{Namespace: namespace, kubeClient: kubeClient} outBuffer := &bytes.Buffer{} - err := c.OutputContainerLogsForPodList(&somePodList, namespace, outBuffer) + outBufferFunc := func(_, _, _ string) io.Writer { return outBuffer } + err := c.OutputContainerLogsForPodList(&somePodList, namespace, outBufferFunc) clientAssertions := assert.New(t) clientAssertions.NoError(err) clientAssertions.Equal("fake logsfake logsfake logs", outBuffer.String()) diff --git a/pkg/kube/fake/printer.go b/pkg/kube/fake/printer.go index 65515812e..dcce9a3be 100644 --- a/pkg/kube/fake/printer.go +++ b/pkg/kube/fake/printer.go @@ -124,7 +124,7 @@ func (p *PrintingKubeClient) GetPodList(_ string, _ metav1.ListOptions) (*v1.Pod } // OutputContainerLogsForPodList implements KubeClient OutputContainerLogsForPodList. -func (p *PrintingKubeClient) OutputContainerLogsForPodList(_ *v1.PodList, someNamespace string, _ io.Writer) error { +func (p *PrintingKubeClient) OutputContainerLogsForPodList(_ *v1.PodList, someNamespace string, _ func(namespace, pod, container string) io.Writer) error { _, err := io.Copy(p.LogOutput, strings.NewReader(fmt.Sprintf("attempted to output logs for namespace: %s", someNamespace))) return err } diff --git a/pkg/kube/interface.go b/pkg/kube/interface.go index f701690e3..c9776cacd 100644 --- a/pkg/kube/interface.go +++ b/pkg/kube/interface.go @@ -84,7 +84,7 @@ type InterfaceLogs interface { GetPodList(namespace string, listOptions metav1.ListOptions) (*v1.PodList, error) // OutputContainerLogsForPodList output the logs for a pod list - OutputContainerLogsForPodList(podList *v1.PodList, namespace string, writer io.Writer) error + OutputContainerLogsForPodList(podList *v1.PodList, namespace string, writerFunc func(namespace, pod, container string) io.Writer) error } // InterfaceDeletionPropagation is introduced to avoid breaking backwards compatibility for Interface implementers.