Unverified Commit 024f168b authored by Matthew Fisher's avatar Matthew Fisher Committed by GitHub

Merge pull request #6612 from bacongobbler/rebase-6167

feat(test): add `--logs` to `helm test`
parents 66e0c0b4 9b9dcebe
...@@ -352,6 +352,8 @@ message TestReleaseRequest { ...@@ -352,6 +352,8 @@ message TestReleaseRequest {
bool parallel = 4; bool parallel = 4;
// maximum number of test pods to run in parallel // maximum number of test pods to run in parallel
uint32 max_parallel = 5; uint32 max_parallel = 5;
// logs specifies whether or not to dump the logs from the test pods
bool logs = 6;
} }
// TestReleaseResponse represents a message from executing a test // TestReleaseResponse represents a message from executing a test
......
...@@ -41,6 +41,7 @@ type releaseTestCmd struct { ...@@ -41,6 +41,7 @@ type releaseTestCmd struct {
cleanup bool cleanup bool
parallel bool parallel bool
maxParallel uint32 maxParallel uint32
logs bool
} }
func newReleaseTestCmd(c helm.Interface, out io.Writer) *cobra.Command { func newReleaseTestCmd(c helm.Interface, out io.Writer) *cobra.Command {
...@@ -71,6 +72,7 @@ func newReleaseTestCmd(c helm.Interface, out io.Writer) *cobra.Command { ...@@ -71,6 +72,7 @@ func newReleaseTestCmd(c helm.Interface, out io.Writer) *cobra.Command {
f.BoolVar(&rlsTest.cleanup, "cleanup", false, "Delete test pods upon completion") f.BoolVar(&rlsTest.cleanup, "cleanup", false, "Delete test pods upon completion")
f.BoolVar(&rlsTest.parallel, "parallel", false, "Run test pods in parallel") f.BoolVar(&rlsTest.parallel, "parallel", false, "Run test pods in parallel")
f.Uint32Var(&rlsTest.maxParallel, "max", 20, "Maximum number of test pods to run in parallel") f.Uint32Var(&rlsTest.maxParallel, "max", 20, "Maximum number of test pods to run in parallel")
f.BoolVar(&rlsTest.logs, "logs", false, "Dump the logs from test pods (this runs after all tests are complete, but before any cleanup")
// set defaults from environment // set defaults from environment
settings.InitTLS(f) settings.InitTLS(f)
...@@ -85,6 +87,7 @@ func (t *releaseTestCmd) run() (err error) { ...@@ -85,6 +87,7 @@ func (t *releaseTestCmd) run() (err error) {
helm.ReleaseTestCleanup(t.cleanup), helm.ReleaseTestCleanup(t.cleanup),
helm.ReleaseTestParallel(t.parallel), helm.ReleaseTestParallel(t.parallel),
helm.ReleaseTestMaxParallel(t.maxParallel), helm.ReleaseTestMaxParallel(t.maxParallel),
helm.ReleaseTestLogs(t.logs),
) )
testErr := &testErr{} testErr := &testErr{}
......
...@@ -20,6 +20,7 @@ helm test [RELEASE] [flags] ...@@ -20,6 +20,7 @@ helm test [RELEASE] [flags]
``` ```
--cleanup Delete test pods upon completion --cleanup Delete test pods upon completion
-h, --help help for test -h, --help help for test
--logs Dump the logs from test pods (this runs after all tests are complete, but before any cleanup
--max uint32 Maximum number of test pods to run in parallel (default 20) --max uint32 Maximum number of test pods to run in parallel (default 20)
--parallel Run test pods in parallel --parallel Run test pods in parallel
--timeout int Time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks) (default 300) --timeout int Time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks) (default 300)
...@@ -47,4 +48,4 @@ helm test [RELEASE] [flags] ...@@ -47,4 +48,4 @@ helm test [RELEASE] [flags]
* [helm](helm.md) - The Helm package manager for Kubernetes. * [helm](helm.md) - The Helm package manager for Kubernetes.
###### Auto generated by spf13/cobra on 25-Jul-2019 ###### Auto generated by spf13/cobra on 8-Oct-2019
...@@ -241,6 +241,13 @@ func ReleaseTestMaxParallel(max uint32) ReleaseTestOption { ...@@ -241,6 +241,13 @@ func ReleaseTestMaxParallel(max uint32) ReleaseTestOption {
} }
} }
// ReleaseTestLogs is a boolean value representing whether to dump the logs from test pods
func ReleaseTestLogs(logs bool) ReleaseTestOption {
return func(opts *options) {
opts.testReq.Logs = logs
}
}
// RollbackTimeout specifies the number of seconds before kubernetes calls timeout // RollbackTimeout specifies the number of seconds before kubernetes calls timeout
func RollbackTimeout(timeout int64) RollbackOption { func RollbackTimeout(timeout int64) RollbackOption {
return func(opts *options) { return func(opts *options) {
......
...@@ -31,7 +31,7 @@ import ( ...@@ -31,7 +31,7 @@ import (
"k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/meta"
"github.com/evanphx/json-patch" jsonpatch "github.com/evanphx/json-patch"
appsv1 "k8s.io/api/apps/v1" appsv1 "k8s.io/api/apps/v1"
appsv1beta1 "k8s.io/api/apps/v1beta1" appsv1beta1 "k8s.io/api/apps/v1beta1"
appsv1beta2 "k8s.io/api/apps/v1beta2" appsv1beta2 "k8s.io/api/apps/v1beta2"
...@@ -935,7 +935,7 @@ func (c *Client) WaitAndGetCompletedPodPhase(namespace string, reader io.Reader, ...@@ -935,7 +935,7 @@ func (c *Client) WaitAndGetCompletedPodPhase(namespace string, reader io.Reader,
} }
func (c *Client) watchPodUntilComplete(timeout time.Duration, info *resource.Info) error { func (c *Client) watchPodUntilComplete(timeout time.Duration, info *resource.Info) error {
lw := cachetools.NewListWatchFromClient(info.Client, info.Mapping.Resource.Resource, info.Namespace, fields.Everything()) lw := cachetools.NewListWatchFromClient(info.Client, info.Mapping.Resource.Resource, info.Namespace, fields.ParseSelectorOrDie(fmt.Sprintf("metadata.name=%s", info.Name)))
c.Log("Watching pod %s for completion with timeout of %v", info.Name, timeout) c.Log("Watching pod %s for completion with timeout of %v", info.Name, timeout)
ctx, cancel := watchtools.ContextWithOptionalTimeout(context.Background(), timeout) ctx, cancel := watchtools.ContextWithOptionalTimeout(context.Background(), timeout)
...@@ -947,6 +947,20 @@ func (c *Client) watchPodUntilComplete(timeout time.Duration, info *resource.Inf ...@@ -947,6 +947,20 @@ func (c *Client) watchPodUntilComplete(timeout time.Duration, info *resource.Inf
return err return err
} }
// GetPodLogs takes pod name and namespace and returns the current logs (streaming is NOT enabled).
func (c *Client) GetPodLogs(name, ns string) (io.ReadCloser, error) {
client, err := c.KubernetesClientSet()
if err != nil {
return nil, err
}
req := client.CoreV1().Pods(ns).GetLogs(name, &v1.PodLogOptions{})
logReader, err := req.Stream()
if err != nil {
return nil, fmt.Errorf("error in opening log stream, got: %s", err)
}
return logReader, nil
}
func isPodComplete(event watch.Event) (bool, error) { func isPodComplete(event watch.Event) (bool, error) {
o, ok := event.Object.(*v1.Pod) o, ok := event.Object.(*v1.Pod)
if !ok { if !ok {
......
This diff is collapsed.
...@@ -19,17 +19,22 @@ package releasetesting ...@@ -19,17 +19,22 @@ package releasetesting
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"io/ioutil"
"log" "log"
"strings"
"sync" "sync"
"time" "time"
"k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
"k8s.io/helm/pkg/proto/hapi/release" "k8s.io/helm/pkg/proto/hapi/release"
"k8s.io/helm/pkg/proto/hapi/services" "k8s.io/helm/pkg/proto/hapi/services"
"k8s.io/helm/pkg/tiller/environment" "k8s.io/helm/pkg/tiller/environment"
) )
// logEscaper is necessary for escaping control characters found in the log stream.
var logEscaper = strings.NewReplacer("%", "%%")
// Environment encapsulates information about where test suite executes and returns results // Environment encapsulates information about where test suite executes and returns results
type Environment struct { type Environment struct {
Namespace string Namespace string
...@@ -126,3 +131,31 @@ func (env *Environment) DeleteTestPods(testManifests []string) { ...@@ -126,3 +131,31 @@ func (env *Environment) DeleteTestPods(testManifests []string) {
} }
} }
} }
// GetLogs collects the logs from the pods created in testManifests
func (env *Environment) GetLogs(testManifests []string) {
for _, testManifest := range testManifests {
infos, err := env.KubeClient.Build(env.Namespace, bytes.NewBufferString(testManifest))
if err != nil {
env.streamError(err.Error())
continue
}
if len(infos) == 0 {
env.streamError(fmt.Sprint("Pod manifest is invalid. Unable to obtain the logs"))
continue
}
podName := infos[0].Object.(*v1.Pod).Name
lr, err := env.KubeClient.GetPodLogs(podName, env.Namespace)
if err != nil {
env.streamError(err.Error())
continue
}
logs, err := ioutil.ReadAll(lr)
if err != nil {
env.streamError(err.Error())
continue
}
msg := fmt.Sprintf("\nPOD LOGS: %s\n%s", podName, logEscaper.Replace(string(logs)))
env.streamMessage(msg, release.TestRun_RUNNING)
}
}
...@@ -89,6 +89,24 @@ func TestDeleteTestPodsFailingDelete(t *testing.T) { ...@@ -89,6 +89,24 @@ func TestDeleteTestPodsFailingDelete(t *testing.T) {
} }
} }
func TestGetTestPodLogs(t *testing.T) {
mockTestSuite := testSuiteFixture([]string{manifestWithTestSuccessHook})
mockTestEnv := newMockTestingEnvironment()
mockTestEnv.KubeClient = newGetLogKubeClient()
mockTestEnv.GetLogs(mockTestSuite.TestManifests)
expectedMessage := "ERROR: Pod manifest is invalid. Unable to obtain the logs"
stream := mockTestEnv.Stream.(*mockStream)
if len(stream.messages) != 1 {
t.Errorf("Expected 1 message, got: %v", len(stream.messages))
}
if stream.messages[0].Msg != expectedMessage {
t.Errorf("Expected message '%s', got: %v", expectedMessage, stream.messages[0].Msg)
}
}
func TestStreamMessage(t *testing.T) { func TestStreamMessage(t *testing.T) {
mockTestEnv := newMockTestingEnvironment() mockTestEnv := newMockTestingEnvironment()
...@@ -181,3 +199,13 @@ func newCreateFailingKubeClient() *createFailingKubeClient { ...@@ -181,3 +199,13 @@ func newCreateFailingKubeClient() *createFailingKubeClient {
func (p *createFailingKubeClient) Create(ns string, r io.Reader, t int64, shouldWait bool) error { func (p *createFailingKubeClient) Create(ns string, r io.Reader, t int64, shouldWait bool) error {
return errors.New("We ran out of budget and couldn't create finding-nemo") return errors.New("We ran out of budget and couldn't create finding-nemo")
} }
type getLogKubeClient struct {
tillerEnv.PrintingKubeClient
}
func newGetLogKubeClient() *getLogKubeClient {
return &getLogKubeClient{
PrintingKubeClient: tillerEnv.PrintingKubeClient{Out: ioutil.Discard},
}
}
...@@ -175,6 +175,8 @@ type KubeClient interface { ...@@ -175,6 +175,8 @@ type KubeClient interface {
// and returns said phase (PodSucceeded or PodFailed qualify). // and returns said phase (PodSucceeded or PodFailed qualify).
WaitAndGetCompletedPodPhase(namespace string, reader io.Reader, timeout time.Duration) (v1.PodPhase, error) WaitAndGetCompletedPodPhase(namespace string, reader io.Reader, timeout time.Duration) (v1.PodPhase, error)
GetPodLogs(name, namespace string) (io.ReadCloser, error)
WaitUntilCRDEstablished(reader io.Reader, timeout time.Duration) error WaitUntilCRDEstablished(reader io.Reader, timeout time.Duration) error
} }
...@@ -255,6 +257,11 @@ func (p *PrintingKubeClient) WaitAndGetCompletedPodPhase(namespace string, reade ...@@ -255,6 +257,11 @@ func (p *PrintingKubeClient) WaitAndGetCompletedPodPhase(namespace string, reade
return v1.PodUnknown, err return v1.PodUnknown, err
} }
// GetPodLogs implements KubeClient GetPodLogs.
func (p *PrintingKubeClient) GetPodLogs(name, ns string) (io.ReadCloser, error) {
return nil, nil
}
// WaitUntilCRDEstablished implements KubeClient WaitUntilCRDEstablished. // WaitUntilCRDEstablished implements KubeClient WaitUntilCRDEstablished.
func (p *PrintingKubeClient) WaitUntilCRDEstablished(reader io.Reader, timeout time.Duration) error { func (p *PrintingKubeClient) WaitUntilCRDEstablished(reader io.Reader, timeout time.Duration) error {
_, err := io.Copy(p.Out, reader) _, err := io.Copy(p.Out, reader)
......
...@@ -78,6 +78,10 @@ func (k *mockKubeClient) WaitAndGetCompletedPodStatus(namespace string, reader i ...@@ -78,6 +78,10 @@ func (k *mockKubeClient) WaitAndGetCompletedPodStatus(namespace string, reader i
return "", nil return "", nil
} }
func (k *mockKubeClient) GetPodLogs(name, namespace string) (io.ReadCloser, error) {
return nil, nil
}
func (k *mockKubeClient) WaitUntilCRDEstablished(reader io.Reader, timeout time.Duration) error { func (k *mockKubeClient) WaitUntilCRDEstablished(reader io.Reader, timeout time.Duration) error {
return nil return nil
} }
......
...@@ -679,6 +679,9 @@ func (kc *mockHooksKubeClient) Validate(ns string, reader io.Reader) error { ...@@ -679,6 +679,9 @@ func (kc *mockHooksKubeClient) Validate(ns string, reader io.Reader) error {
func (kc *mockHooksKubeClient) WaitAndGetCompletedPodPhase(namespace string, reader io.Reader, timeout time.Duration) (v1.PodPhase, error) { func (kc *mockHooksKubeClient) WaitAndGetCompletedPodPhase(namespace string, reader io.Reader, timeout time.Duration) (v1.PodPhase, error) {
return v1.PodUnknown, nil return v1.PodUnknown, nil
} }
func (kc *mockHooksKubeClient) GetPodLogs(name, namespace string) (io.ReadCloser, error) {
return nil, nil
}
func (kc *mockHooksKubeClient) WaitUntilCRDEstablished(reader io.Reader, timeout time.Duration) error { func (kc *mockHooksKubeClient) WaitUntilCRDEstablished(reader io.Reader, timeout time.Duration) error {
return nil return nil
......
...@@ -69,6 +69,10 @@ func (s *ReleaseServer) RunReleaseTest(req *services.TestReleaseRequest, stream ...@@ -69,6 +69,10 @@ func (s *ReleaseServer) RunReleaseTest(req *services.TestReleaseRequest, stream
Results: tSuite.Results, Results: tSuite.Results,
} }
if req.Logs {
testEnv.GetLogs(tSuite.TestManifests)
}
if req.Cleanup { if req.Cleanup {
testEnv.DeleteTestPods(tSuite.TestManifests) testEnv.DeleteTestPods(tSuite.TestManifests)
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment