Merge pull request #10309 from Bez625/main

Add hook annotation to output hook logs to client on error
pull/30576/head
Scott Rigby 7 months ago committed by GitHub
commit 2cda65d444
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -57,6 +57,11 @@ func warning(format string, v ...interface{}) {
fmt.Fprintf(os.Stderr, format, v...) fmt.Fprintf(os.Stderr, format, v...)
} }
// hookOutputWriter provides the writer for writing hook logs.
func hookOutputWriter(_, _, _ string) io.Writer {
return log.Writer()
}
func main() { func main() {
// Setting the name of the app for managedFields in the Kubernetes client. // Setting the name of the app for managedFields in the Kubernetes client.
// It is set here to the full name of "helm" so that renaming of helm to // It is set here to the full name of "helm" so that renaming of helm to
@ -80,6 +85,7 @@ func main() {
if helmDriver == "memory" { if helmDriver == "memory" {
loadReleasesInMemory(actionConfig) loadReleasesInMemory(actionConfig)
} }
actionConfig.SetHookOutputFunc(hookOutputWriter)
}) })
if err := cmd.Execute(); err != nil { if err := cmd.Execute(); err != nil {

@ -19,6 +19,7 @@ package action
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"io"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
@ -95,6 +96,9 @@ type Configuration struct {
Capabilities *chartutil.Capabilities Capabilities *chartutil.Capabilities
Log func(string, ...interface{}) 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 // renderResources renders the templates in a chart
@ -122,7 +126,7 @@ func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Valu
var err2 error var err2 error
// A `helm template` should not talk to the remote cluster. However, commands with the flag // 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. // It may break in interesting and exotic ways because other data (e.g. discovery) is mocked.
if interactWithRemote && cfg.RESTClientGetter != nil { if interactWithRemote && cfg.RESTClientGetter != nil {
restConfig, err := cfg.RESTClientGetter.ToRESTConfig() restConfig, err := cfg.RESTClientGetter.ToRESTConfig()
@ -422,6 +426,12 @@ func (cfg *Configuration) Init(getter genericclioptions.RESTClientGetter, namesp
cfg.KubeClient = kc cfg.KubeClient = kc
cfg.Releases = store cfg.Releases = store
cfg.Log = log cfg.Log = log
cfg.HookOutputFunc = func(_, _, _ string) io.Writer { return io.Discard }
return nil return nil
} }
// SetHookOutputFunc sets the HookOutputFunc on the Configuration.
func (cfg *Configuration) SetHookOutputFunc(hookOutputFunc func(_, _, _ string) io.Writer) {
cfg.HookOutputFunc = hookOutputFunc
}

@ -111,6 +111,14 @@ type chartOptions struct {
type chartOption func(*chartOptions) type chartOption func(*chartOptions)
func buildChart(opts ...chartOption) *chart.Chart { func buildChart(opts ...chartOption) *chart.Chart {
defaultTemplates := []*chart.File{
{Name: "templates/hello", Data: []byte("hello: world")},
{Name: "templates/hooks", Data: []byte(manifestWithHook)},
}
return buildChartWithTemplates(defaultTemplates, opts...)
}
func buildChartWithTemplates(templates []*chart.File, opts ...chartOption) *chart.Chart {
c := &chartOptions{ c := &chartOptions{
Chart: &chart.Chart{ Chart: &chart.Chart{
// TODO: This should be more complete. // TODO: This should be more complete.
@ -119,18 +127,13 @@ func buildChart(opts ...chartOption) *chart.Chart {
Name: "hello", Name: "hello",
Version: "0.1.0", Version: "0.1.0",
}, },
// This adds a basic template and hooks. Templates: templates,
Templates: []*chart.File{
{Name: "templates/hello", Data: []byte("hello: world")},
{Name: "templates/hooks", Data: []byte(manifestWithHook)},
},
}, },
} }
for _, opt := range opts { for _, opt := range opts {
opt(c) opt(c)
} }
return c.Chart return c.Chart
} }

@ -17,12 +17,19 @@ package action
import ( import (
"bytes" "bytes"
"fmt"
"log"
"slices"
"sort" "sort"
"time" "time"
"helm.sh/helm/v4/pkg/kube"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/pkg/errors" "github.com/pkg/errors"
"gopkg.in/yaml.v3"
"helm.sh/helm/v4/pkg/kube"
"helm.sh/helm/v4/pkg/release" "helm.sh/helm/v4/pkg/release"
helmtime "helm.sh/helm/v4/pkg/time" helmtime "helm.sh/helm/v4/pkg/time"
) )
@ -87,10 +94,16 @@ func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent,
// 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
// If a hook is failed, check the annotation of the hook to determine if we should copy the logs client side
if errOutputting := cfg.outputLogsByPolicy(h, rl.Namespace, release.HookOutputOnFailed); errOutputting != nil {
// We log the error here as we want to propagate the hook failure upwards to the release object.
log.Printf("error outputting logs for hook failure: %v", errOutputting)
}
// If a hook is failed, check the annotation of the hook to determine whether the hook should be deleted // If a hook is failed, check 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 // under failed condition. If so, then clear the corresponding resource object in the hook
if err := cfg.deleteHookByPolicy(h, release.HookFailed, timeout); err != nil { if errDeleting := cfg.deleteHookByPolicy(h, release.HookFailed, timeout); errDeleting != nil {
return err // We log the error here as we want to propagate the hook failure upwards to the release object.
log.Printf("error deleting the hook resource on hook failure: %v", errDeleting)
} }
return err return err
} }
@ -98,9 +111,13 @@ func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent,
} }
// If all hooks are successful, check the annotation of each hook to determine whether the hook should be deleted // If all hooks are successful, check 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 // or output should be logged under succeeded condition. If so, then clear the corresponding resource object in each hook
for i := len(executingHooks) - 1; i >= 0; i-- { for i := len(executingHooks) - 1; i >= 0; i-- {
h := executingHooks[i] h := executingHooks[i]
if err := cfg.outputLogsByPolicy(h, rl.Namespace, release.HookOutputOnSucceeded); err != nil {
// We log here as we still want to attempt hook resource deletion even if output logging fails.
log.Printf("error outputting logs for hook failure: %v", err)
}
if err := cfg.deleteHookByPolicy(h, release.HookSucceeded, timeout); err != nil { if err := cfg.deleteHookByPolicy(h, release.HookSucceeded, timeout); err != nil {
return err return err
} }
@ -138,7 +155,7 @@ func (cfg *Configuration) deleteHookByPolicy(h *release.Hook, policy release.Hoo
return errors.New(joinErrors(errs)) return errors.New(joinErrors(errs))
} }
//wait for resources until they are deleted to avoid conflicts // wait for resources until they are deleted to avoid conflicts
if kubeClient, ok := cfg.KubeClient.(kube.InterfaceExt); ok { if kubeClient, ok := cfg.KubeClient.(kube.InterfaceExt); ok {
if err := kubeClient.WaitForDelete(resources, timeout); err != nil { if err := kubeClient.WaitForDelete(resources, timeout); err != nil {
return err return err
@ -158,3 +175,57 @@ func hookHasDeletePolicy(h *release.Hook, policy release.HookDeletePolicy) bool
} }
return false return false
} }
// 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
}
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
}
}
func (cfg *Configuration) outputContainerLogsForListOptions(namespace string, listOptions metav1.ListOptions) error {
// TODO Helm 4: Remove this check when GetPodList and OutputContainerLogsForPodList are moved from InterfaceLogs to Interface
if kubeClient, ok := cfg.KubeClient.(kube.InterfaceLogs); ok {
podList, err := kubeClient.GetPodList(namespace, listOptions)
if err != nil {
return err
}
err = kubeClient.OutputContainerLogsForPodList(podList, namespace, cfg.HookOutputFunc)
return err
}
return nil
}
func (cfg *Configuration) deriveNamespace(h *release.Hook, namespace string) (string, error) {
tmp := struct {
Metadata struct {
Namespace string
}
}{}
err := yaml.Unmarshal([]byte(h.Manifest), &tmp)
if err != nil {
return "", errors.Wrapf(err, "unable to parse metadata.namespace from kubernetes manifest for output logs hook %s", h.Path)
}
if tmp.Metadata.Namespace == "" {
return namespace, nil
}
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 {
return slices.Contains(h.OutputLogPolicies, policy)
}

@ -0,0 +1,208 @@
/*
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"
"fmt"
"io"
"testing"
"github.com/stretchr/testify/assert"
"helm.sh/helm/v4/pkg/chart"
kubefake "helm.sh/helm/v4/pkg/kube/fake"
"helm.sh/helm/v4/pkg/release"
)
func podManifestWithOutputLogs(hookDefinitions []release.HookOutputLogPolicy) string {
hookDefinitionString := convertHooksToCommaSeparated(hookDefinitions)
return fmt.Sprintf(`kind: Pod
metadata:
name: finding-sharky,
annotations:
"helm.sh/hook": pre-install
"helm.sh/hook-output-log-policy": %s
spec:
containers:
- name: sharky-test
image: fake-image
cmd: fake-command`, hookDefinitionString)
}
func podManifestWithOutputLogWithNamespace(hookDefinitions []release.HookOutputLogPolicy) string {
hookDefinitionString := convertHooksToCommaSeparated(hookDefinitions)
return fmt.Sprintf(`kind: Pod
metadata:
name: finding-george
namespace: sneaky-namespace
annotations:
"helm.sh/hook": pre-install
"helm.sh/hook-output-log-policy": %s
spec:
containers:
- name: george-test
image: fake-image
cmd: fake-command`, hookDefinitionString)
}
func jobManifestWithOutputLog(hookDefinitions []release.HookOutputLogPolicy) string {
hookDefinitionString := convertHooksToCommaSeparated(hookDefinitions)
return fmt.Sprintf(`kind: Job
apiVersion: batch/v1
metadata:
name: losing-religion
annotations:
"helm.sh/hook": pre-install
"helm.sh/hook-output-log-policy": %s
spec:
completions: 1
parallelism: 1
activeDeadlineSeconds: 30
template:
spec:
containers:
- name: religion-container
image: religion-image
cmd: religion-command`, hookDefinitionString)
}
func jobManifestWithOutputLogWithNamespace(hookDefinitions []release.HookOutputLogPolicy) string {
hookDefinitionString := convertHooksToCommaSeparated(hookDefinitions)
return fmt.Sprintf(`kind: Job
apiVersion: batch/v1
metadata:
name: losing-religion
namespace: rem-namespace
annotations:
"helm.sh/hook": pre-install
"helm.sh/hook-output-log-policy": %s
spec:
completions: 1
parallelism: 1
activeDeadlineSeconds: 30
template:
spec:
containers:
- name: religion-container
image: religion-image
cmd: religion-command`, hookDefinitionString)
}
func convertHooksToCommaSeparated(hookDefinitions []release.HookOutputLogPolicy) string {
var commaSeparated string
for i, policy := range hookDefinitions {
if i+1 == len(hookDefinitions) {
commaSeparated += policy.String()
} else {
commaSeparated += policy.String() + ","
}
}
return commaSeparated
}
func TestInstallRelease_HookOutputLogsOnFailure(t *testing.T) {
// Should output on failure with expected namespace if hook-failed is set
runInstallForHooksWithFailure(t, podManifestWithOutputLogs([]release.HookOutputLogPolicy{release.HookOutputOnFailed}), "spaced", true)
runInstallForHooksWithFailure(t, podManifestWithOutputLogWithNamespace([]release.HookOutputLogPolicy{release.HookOutputOnFailed}), "sneaky-namespace", true)
runInstallForHooksWithFailure(t, jobManifestWithOutputLog([]release.HookOutputLogPolicy{release.HookOutputOnFailed}), "spaced", true)
runInstallForHooksWithFailure(t, jobManifestWithOutputLogWithNamespace([]release.HookOutputLogPolicy{release.HookOutputOnFailed}), "rem-namespace", true)
// Should not output on failure with expected namespace if hook-succeed is set
runInstallForHooksWithFailure(t, podManifestWithOutputLogs([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded}), "", false)
runInstallForHooksWithFailure(t, podManifestWithOutputLogWithNamespace([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded}), "", false)
runInstallForHooksWithFailure(t, jobManifestWithOutputLog([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded}), "", false)
runInstallForHooksWithFailure(t, jobManifestWithOutputLogWithNamespace([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded}), "", false)
}
func TestInstallRelease_HookOutputLogsOnSuccess(t *testing.T) {
// Should output on success with expected namespace if hook-succeeded is set
runInstallForHooksWithSuccess(t, podManifestWithOutputLogs([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded}), "spaced", true)
runInstallForHooksWithSuccess(t, podManifestWithOutputLogWithNamespace([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded}), "sneaky-namespace", true)
runInstallForHooksWithSuccess(t, jobManifestWithOutputLog([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded}), "spaced", true)
runInstallForHooksWithSuccess(t, jobManifestWithOutputLogWithNamespace([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded}), "rem-namespace", true)
// Should not output on success if hook-failed is set
runInstallForHooksWithSuccess(t, podManifestWithOutputLogs([]release.HookOutputLogPolicy{release.HookOutputOnFailed}), "", false)
runInstallForHooksWithSuccess(t, podManifestWithOutputLogWithNamespace([]release.HookOutputLogPolicy{release.HookOutputOnFailed}), "", false)
runInstallForHooksWithSuccess(t, jobManifestWithOutputLog([]release.HookOutputLogPolicy{release.HookOutputOnFailed}), "", false)
runInstallForHooksWithSuccess(t, jobManifestWithOutputLogWithNamespace([]release.HookOutputLogPolicy{release.HookOutputOnFailed}), "", false)
}
func TestInstallRelease_HooksOutputLogsOnSuccessAndFailure(t *testing.T) {
// Should output on success with expected namespace if hook-succeeded and hook-failed is set
runInstallForHooksWithSuccess(t, podManifestWithOutputLogs([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded, release.HookOutputOnFailed}), "spaced", true)
runInstallForHooksWithSuccess(t, podManifestWithOutputLogWithNamespace([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded, release.HookOutputOnFailed}), "sneaky-namespace", true)
runInstallForHooksWithSuccess(t, jobManifestWithOutputLog([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded, release.HookOutputOnFailed}), "spaced", true)
runInstallForHooksWithSuccess(t, jobManifestWithOutputLogWithNamespace([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded, release.HookOutputOnFailed}), "rem-namespace", true)
// Should output on failure if hook-succeeded and hook-failed is set
runInstallForHooksWithFailure(t, podManifestWithOutputLogs([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded, release.HookOutputOnFailed}), "spaced", true)
runInstallForHooksWithFailure(t, podManifestWithOutputLogWithNamespace([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded, release.HookOutputOnFailed}), "sneaky-namespace", true)
runInstallForHooksWithFailure(t, jobManifestWithOutputLog([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded, release.HookOutputOnFailed}), "spaced", true)
runInstallForHooksWithFailure(t, jobManifestWithOutputLogWithNamespace([]release.HookOutputLogPolicy{release.HookOutputOnSucceeded, release.HookOutputOnFailed}), "rem-namespace", true)
}
func runInstallForHooksWithSuccess(t *testing.T, manifest, expectedNamespace string, shouldOutput bool) {
var expectedOutput string
if shouldOutput {
expectedOutput = fmt.Sprintf("attempted to output logs for namespace: %s", expectedNamespace)
}
is := assert.New(t)
instAction := installAction(t)
instAction.ReleaseName = "failed-hooks"
outBuffer := &bytes.Buffer{}
instAction.cfg.KubeClient = &kubefake.PrintingKubeClient{Out: io.Discard, LogOutput: outBuffer}
templates := []*chart.File{
{Name: "templates/hello", Data: []byte("hello: world")},
{Name: "templates/hooks", Data: []byte(manifest)},
}
vals := map[string]interface{}{}
res, err := instAction.Run(buildChartWithTemplates(templates), vals)
is.NoError(err)
is.Equal(expectedOutput, outBuffer.String())
is.Equal(release.StatusDeployed, res.Info.Status)
}
func runInstallForHooksWithFailure(t *testing.T, manifest, expectedNamespace string, shouldOutput bool) {
var expectedOutput string
if shouldOutput {
expectedOutput = fmt.Sprintf("attempted to output logs for namespace: %s", expectedNamespace)
}
is := assert.New(t)
instAction := installAction(t)
instAction.ReleaseName = "failed-hooks"
failingClient := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failingClient.WatchUntilReadyError = fmt.Errorf("failed watch")
instAction.cfg.KubeClient = failingClient
outBuffer := &bytes.Buffer{}
failingClient.PrintingKubeClient = kubefake.PrintingKubeClient{Out: io.Discard, LogOutput: outBuffer}
templates := []*chart.File{
{Name: "templates/hello", Data: []byte("hello: world")},
{Name: "templates/hooks", Data: []byte(manifest)},
}
vals := map[string]interface{}{}
res, err := instAction.Run(buildChartWithTemplates(templates), vals)
is.Error(err)
is.Contains(res.Info.Description, "failed pre-install")
is.Equal(expectedOutput, outBuffer.String())
is.Equal(release.StatusFailed, res.Info.Status)
}

@ -17,6 +17,7 @@ limitations under the License.
package action package action
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"io" "io"
@ -354,11 +355,14 @@ func TestInstallRelease_FailedHooks(t *testing.T) {
failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient) failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient)
failer.WatchUntilReadyError = fmt.Errorf("Failed watch") failer.WatchUntilReadyError = fmt.Errorf("Failed watch")
instAction.cfg.KubeClient = failer instAction.cfg.KubeClient = failer
outBuffer := &bytes.Buffer{}
failer.PrintingKubeClient = kubefake.PrintingKubeClient{Out: io.Discard, LogOutput: outBuffer}
vals := map[string]interface{}{} vals := map[string]interface{}{}
res, err := instAction.Run(buildChart(), vals) res, err := instAction.Run(buildChart(), vals)
is.Error(err) is.Error(err)
is.Contains(res.Info.Description, "failed post-install") is.Contains(res.Info.Description, "failed post-install")
is.Equal("", outBuffer.String())
is.Equal(release.StatusFailed, res.Info.Status) is.Equal(release.StatusFailed, res.Info.Status)
} }

@ -83,7 +83,7 @@ type Client struct {
// Namespace allows to bypass the kubeconfig file for the choice of the namespace // Namespace allows to bypass the kubeconfig file for the choice of the namespace
Namespace string Namespace string
kubeClient *kubernetes.Clientset kubeClient kubernetes.Interface
} }
func init() { func init() {
@ -111,7 +111,7 @@ func New(getter genericclioptions.RESTClientGetter) *Client {
var nopLogger = func(_ string, _ ...interface{}) {} var nopLogger = func(_ string, _ ...interface{}) {}
// getKubeClient get or create a new KubernetesClientSet // getKubeClient get or create a new KubernetesClientSet
func (c *Client) getKubeClient() (*kubernetes.Clientset, error) { func (c *Client) getKubeClient() (kubernetes.Interface, error) {
var err error var err error
if c.kubeClient == nil { if c.kubeClient == nil {
c.kubeClient, err = c.Factory.KubernetesClientSet() c.kubeClient, err = c.Factory.KubernetesClientSet()
@ -131,7 +131,7 @@ func (c *Client) IsReachable() error {
if err != nil { if err != nil {
return errors.Wrap(err, "Kubernetes cluster unreachable") return errors.Wrap(err, "Kubernetes cluster unreachable")
} }
if _, err := client.ServerVersion(); err != nil { if _, err := client.Discovery().ServerVersion(); err != nil {
return errors.Wrap(err, "Kubernetes cluster unreachable") return errors.Wrap(err, "Kubernetes cluster unreachable")
} }
return nil return nil
@ -812,6 +812,48 @@ func (c *Client) waitForPodSuccess(obj runtime.Object, name string) (bool, error
return false, nil return false, nil
} }
// GetPodList uses the kubernetes interface to get the list of pods filtered by listOptions
func (c *Client) GetPodList(namespace string, listOptions metav1.ListOptions) (*v1.PodList, error) {
podList, err := c.kubeClient.CoreV1().Pods(namespace).List(context.Background(), listOptions)
if err != nil {
return nil, fmt.Errorf("failed to get pod list with options: %+v with error: %v", listOptions, err)
}
return podList, nil
}
// OutputContainerLogsForPodList is a helper that outputs logs for a list of pods
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, writerFunc(namespace, pod.Name, container.Name))
if err2 != nil {
return err2
}
}
}
return nil
}
func copyRequestStreamToWriter(request *rest.Request, podName, containerName string, writer io.Writer) error {
readCloser, err := request.Stream(context.Background())
if err != nil {
return errors.Errorf("Failed to stream pod logs for pod: %s, container: %s", podName, containerName)
}
defer readCloser.Close()
_, err = io.Copy(writer, readCloser)
if err != nil {
return errors.Errorf("Failed to copy IO from logs for pod: %s, container: %s", podName, containerName)
}
if err != nil {
return errors.Errorf("Failed to close reader for pod: %s, container: %s", podName, containerName)
}
return nil
}
// scrubValidationError removes kubectl info from the message. // scrubValidationError removes kubectl info from the message.
func scrubValidationError(err error) error { func scrubValidationError(err error) error {
if err == nil { if err == nil {

@ -24,10 +24,13 @@ import (
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/cli-runtime/pkg/resource" "k8s.io/cli-runtime/pkg/resource"
k8sfake "k8s.io/client-go/kubernetes/fake"
"k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest/fake" "k8s.io/client-go/rest/fake"
cmdtesting "k8s.io/kubectl/pkg/cmd/testing" cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
@ -682,6 +685,39 @@ func TestReal(t *testing.T) {
} }
} }
func TestGetPodList(t *testing.T) {
namespace := "some-namespace"
names := []string{"dave", "jimmy"}
var responsePodList v1.PodList
for _, name := range names {
responsePodList.Items = append(responsePodList.Items, newPodWithStatus(name, v1.PodStatus{}, namespace))
}
kubeClient := k8sfake.NewSimpleClientset(&responsePodList)
c := Client{Namespace: namespace, kubeClient: kubeClient}
podList, err := c.GetPodList(namespace, metav1.ListOptions{})
clientAssertions := assert.New(t)
clientAssertions.NoError(err)
clientAssertions.Equal(&responsePodList, podList)
}
func TestOutputContainerLogsForPodList(t *testing.T) {
namespace := "some-namespace"
somePodList := newPodList("jimmy", "three", "structs")
kubeClient := k8sfake.NewSimpleClientset(&somePodList)
c := Client{Namespace: namespace, kubeClient: kubeClient}
outBuffer := &bytes.Buffer{}
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())
}
const testServiceManifest = ` const testServiceManifest = `
kind: Service kind: Service
apiVersion: v1 apiVersion: v1

@ -17,10 +17,12 @@ limitations under the License.
package fake package fake
import ( import (
"fmt"
"io" "io"
"strings" "strings"
"time" "time"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/cli-runtime/pkg/resource" "k8s.io/cli-runtime/pkg/resource"
@ -31,7 +33,8 @@ import (
// PrintingKubeClient implements KubeClient, but simply prints the reader to // PrintingKubeClient implements KubeClient, but simply prints the reader to
// the given output. // the given output.
type PrintingKubeClient struct { type PrintingKubeClient struct {
Out io.Writer Out io.Writer
LogOutput io.Writer
} }
// IsReachable checks if the cluster is reachable // IsReachable checks if the cluster is reachable
@ -110,6 +113,22 @@ func (p *PrintingKubeClient) BuildTable(_ io.Reader, _ bool) (kube.ResourceList,
return []*resource.Info{}, nil return []*resource.Info{}, nil
} }
// WaitAndGetCompletedPodPhase implements KubeClient WaitAndGetCompletedPodPhase.
func (p *PrintingKubeClient) WaitAndGetCompletedPodPhase(_ string, _ time.Duration) (v1.PodPhase, error) {
return v1.PodSucceeded, nil
}
// GetPodList implements KubeClient GetPodList.
func (p *PrintingKubeClient) GetPodList(_ string, _ metav1.ListOptions) (*v1.PodList, error) {
return &v1.PodList{}, nil
}
// OutputContainerLogsForPodList implements KubeClient OutputContainerLogsForPodList.
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
}
// DeleteWithPropagationPolicy implements KubeClient delete. // DeleteWithPropagationPolicy implements KubeClient delete.
// //
// It only prints out the content to be deleted. // It only prints out the content to be deleted.

@ -20,6 +20,7 @@ import (
"io" "io"
"time" "time"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
) )
@ -67,7 +68,7 @@ type Interface interface {
IsReachable() error IsReachable() error
} }
// InterfaceExt is introduced to avoid breaking backwards compatibility for Interface implementers. // InterfaceExt was introduced to avoid breaking backwards compatibility for Interface implementers.
// //
// TODO Helm 4: Remove InterfaceExt and integrate its method(s) into the Interface. // TODO Helm 4: Remove InterfaceExt and integrate its method(s) into the Interface.
type InterfaceExt interface { type InterfaceExt interface {
@ -75,11 +76,22 @@ type InterfaceExt interface {
WaitForDelete(resources ResourceList, timeout time.Duration) error WaitForDelete(resources ResourceList, timeout time.Duration) error
} }
// InterfaceLogs was introduced to avoid breaking backwards compatibility for Interface implementers.
//
// TODO Helm 4: Remove InterfaceLogs and integrate its method(s) into the Interface.
type InterfaceLogs interface {
// GetPodList list all pods that match the specified listOptions
GetPodList(namespace string, listOptions metav1.ListOptions) (*v1.PodList, error)
// OutputContainerLogsForPodList output the logs for a pod list
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. // InterfaceDeletionPropagation is introduced to avoid breaking backwards compatibility for Interface implementers.
// //
// TODO Helm 4: Remove InterfaceDeletionPropagation and integrate its method(s) into the Interface. // TODO Helm 4: Remove InterfaceDeletionPropagation and integrate its method(s) into the Interface.
type InterfaceDeletionPropagation interface { type InterfaceDeletionPropagation interface {
// Delete destroys one or more resources. The deletion propagation is handled as per the given deletion propagation value. // DeleteWithPropagationPolicy destroys one or more resources. The deletion propagation is handled as per the given deletion propagation value.
DeleteWithPropagationPolicy(resources ResourceList, policy metav1.DeletionPropagation) (*Result, []error) DeleteWithPropagationPolicy(resources ResourceList, policy metav1.DeletionPropagation) (*Result, []error)
} }
@ -107,5 +119,6 @@ type InterfaceResources interface {
var _ Interface = (*Client)(nil) var _ Interface = (*Client)(nil)
var _ InterfaceExt = (*Client)(nil) var _ InterfaceExt = (*Client)(nil)
var _ InterfaceLogs = (*Client)(nil)
var _ InterfaceDeletionPropagation = (*Client)(nil) var _ InterfaceDeletionPropagation = (*Client)(nil)
var _ InterfaceResources = (*Client)(nil) var _ InterfaceResources = (*Client)(nil)

@ -50,6 +50,17 @@ const (
func (x HookDeletePolicy) String() string { return string(x) } func (x HookDeletePolicy) String() string { return string(x) }
// HookOutputLogPolicy specifies the hook output log policy
type HookOutputLogPolicy string
// Hook output log policy types
const (
HookOutputOnSucceeded HookOutputLogPolicy = "hook-succeeded"
HookOutputOnFailed HookOutputLogPolicy = "hook-failed"
)
func (x HookOutputLogPolicy) String() string { return string(x) }
// HookAnnotation is the label name for a hook // HookAnnotation is the label name for a hook
const HookAnnotation = "helm.sh/hook" const HookAnnotation = "helm.sh/hook"
@ -59,6 +70,9 @@ const HookWeightAnnotation = "helm.sh/hook-weight"
// HookDeleteAnnotation is the label name for the delete policy for a hook // HookDeleteAnnotation is the label name for the delete policy for a hook
const HookDeleteAnnotation = "helm.sh/hook-delete-policy" const HookDeleteAnnotation = "helm.sh/hook-delete-policy"
// HookOutputLogAnnotation is the label name for the output log policy for a hook
const HookOutputLogAnnotation = "helm.sh/hook-output-log-policy"
// Hook defines a hook object. // Hook defines a hook object.
type Hook struct { type Hook struct {
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
@ -76,6 +90,8 @@ type Hook struct {
Weight int `json:"weight,omitempty"` Weight int `json:"weight,omitempty"`
// DeletePolicies are the policies that indicate when to delete the hook // DeletePolicies are the policies that indicate when to delete the hook
DeletePolicies []HookDeletePolicy `json:"delete_policies,omitempty"` DeletePolicies []HookDeletePolicy `json:"delete_policies,omitempty"`
// OutputLogPolicies defines whether we should copy hook logs back to main process
OutputLogPolicies []HookOutputLogPolicy `json:"output_log_policies,omitempty"`
} }
// 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.

@ -128,6 +128,14 @@ func SortManifests(files map[string]string, _ chartutil.VersionSet, ordering Kin
// metadata: // metadata:
// annotations: // annotations:
// helm.sh/hook-delete-policy: hook-succeeded // helm.sh/hook-delete-policy: hook-succeeded
//
// To determine the policy to output logs of the hook (for Pod and Job only), it looks for a YAML structure like this:
//
// kind: Pod
// apiVersion: v1
// metadata:
// annotations:
// helm.sh/hook-output-log-policy: hook-succeeded,hook-failed
func (file *manifestFile) sort(result *result) error { func (file *manifestFile) sort(result *result) error {
// Go through manifests in order found in file (function `SplitManifests` creates integer-sortable keys) // Go through manifests in order found in file (function `SplitManifests` creates integer-sortable keys)
var sortedEntryKeys []string var sortedEntryKeys []string
@ -166,13 +174,14 @@ func (file *manifestFile) sort(result *result) error {
hw := calculateHookWeight(entry) hw := calculateHookWeight(entry)
h := &release.Hook{ h := &release.Hook{
Name: entry.Metadata.Name, Name: entry.Metadata.Name,
Kind: entry.Kind, Kind: entry.Kind,
Path: file.path, Path: file.path,
Manifest: m, Manifest: m,
Events: []release.HookEvent{}, Events: []release.HookEvent{},
Weight: hw, Weight: hw,
DeletePolicies: []release.HookDeletePolicy{}, DeletePolicies: []release.HookDeletePolicy{},
OutputLogPolicies: []release.HookOutputLogPolicy{},
} }
isUnknownHook := false isUnknownHook := false
@ -196,6 +205,10 @@ func (file *manifestFile) sort(result *result) error {
operateAnnotationValues(entry, release.HookDeleteAnnotation, func(value string) { operateAnnotationValues(entry, release.HookDeleteAnnotation, func(value string) {
h.DeletePolicies = append(h.DeletePolicies, release.HookDeletePolicy(value)) h.DeletePolicies = append(h.DeletePolicies, release.HookDeletePolicy(value))
}) })
operateAnnotationValues(entry, release.HookOutputLogAnnotation, func(value string) {
h.OutputLogPolicies = append(h.OutputLogPolicies, release.HookOutputLogPolicy(value))
})
} }
return nil return nil

Loading…
Cancel
Save