Always return exec command output
Always return exec command output, even if the command invocation returns nonzero. This applies to exec probes and kubelet RunInContainer calls.
This commit is contained in:
parent
7b762fb34b
commit
c3fe759fec
@ -2880,11 +2880,10 @@ func (kl *Kubelet) RunInContainer(podFullName string, podUID types.UID, containe
|
|||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
output := ioutils.WriteCloserWrapper(&buffer)
|
output := ioutils.WriteCloserWrapper(&buffer)
|
||||||
err = kl.runner.ExecInContainer(container.ID, cmd, nil, output, output, false, nil)
|
err = kl.runner.ExecInContainer(container.ID, cmd, nil, output, output, false, nil)
|
||||||
if err != nil {
|
// Even if err is non-nil, there still may be output (e.g. the exec wrote to stdout or stderr but
|
||||||
return nil, err
|
// the command returned a nonzero exit code). Therefore, always return the output along with the
|
||||||
}
|
// error.
|
||||||
|
return buffer.Bytes(), err
|
||||||
return buffer.Bytes(), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExecInContainer executes a command in a container, connecting the supplied
|
// ExecInContainer executes a command in a container, connecting the supplied
|
||||||
|
@ -18,6 +18,7 @@ package kubelet
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@ -975,6 +976,7 @@ func TestMakeVolumeMounts(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type fakeContainerCommandRunner struct {
|
type fakeContainerCommandRunner struct {
|
||||||
|
// what was passed in
|
||||||
Cmd []string
|
Cmd []string
|
||||||
ID kubecontainer.ContainerID
|
ID kubecontainer.ContainerID
|
||||||
PodID types.UID
|
PodID types.UID
|
||||||
@ -985,15 +987,25 @@ type fakeContainerCommandRunner struct {
|
|||||||
TTY bool
|
TTY bool
|
||||||
Port uint16
|
Port uint16
|
||||||
Stream io.ReadWriteCloser
|
Stream io.ReadWriteCloser
|
||||||
|
|
||||||
|
// what to return
|
||||||
|
StdoutData string
|
||||||
|
StderrData string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeContainerCommandRunner) ExecInContainer(id kubecontainer.ContainerID, cmd []string, in io.Reader, out, err io.WriteCloser, tty bool, resize <-chan term.Size) error {
|
func (f *fakeContainerCommandRunner) ExecInContainer(id kubecontainer.ContainerID, cmd []string, in io.Reader, out, err io.WriteCloser, tty bool, resize <-chan term.Size) error {
|
||||||
|
// record params
|
||||||
f.Cmd = cmd
|
f.Cmd = cmd
|
||||||
f.ID = id
|
f.ID = id
|
||||||
f.Stdin = in
|
f.Stdin = in
|
||||||
f.Stdout = out
|
f.Stdout = out
|
||||||
f.Stderr = err
|
f.Stderr = err
|
||||||
f.TTY = tty
|
f.TTY = tty
|
||||||
|
|
||||||
|
// Copy stdout/stderr data
|
||||||
|
fmt.Fprint(out, f.StdoutData)
|
||||||
|
fmt.Fprint(out, f.StderrData)
|
||||||
|
|
||||||
return f.E
|
return f.E
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1027,35 +1039,45 @@ func TestRunInContainerNoSuchPod(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRunInContainer(t *testing.T) {
|
func TestRunInContainer(t *testing.T) {
|
||||||
testKubelet := newTestKubelet(t, false /* controllerAttachDetachEnabled */)
|
for _, testError := range []error{nil, errors.New("foo")} {
|
||||||
kubelet := testKubelet.kubelet
|
testKubelet := newTestKubelet(t, false /* controllerAttachDetachEnabled */)
|
||||||
fakeRuntime := testKubelet.fakeRuntime
|
kubelet := testKubelet.kubelet
|
||||||
fakeCommandRunner := fakeContainerCommandRunner{}
|
fakeRuntime := testKubelet.fakeRuntime
|
||||||
kubelet.runner = &fakeCommandRunner
|
fakeCommandRunner := fakeContainerCommandRunner{
|
||||||
|
E: testError,
|
||||||
|
StdoutData: "foo",
|
||||||
|
StderrData: "bar",
|
||||||
|
}
|
||||||
|
kubelet.runner = &fakeCommandRunner
|
||||||
|
|
||||||
containerID := kubecontainer.ContainerID{Type: "test", ID: "abc1234"}
|
containerID := kubecontainer.ContainerID{Type: "test", ID: "abc1234"}
|
||||||
fakeRuntime.PodList = []*containertest.FakePod{
|
fakeRuntime.PodList = []*containertest.FakePod{
|
||||||
{Pod: &kubecontainer.Pod{
|
{Pod: &kubecontainer.Pod{
|
||||||
ID: "12345678",
|
ID: "12345678",
|
||||||
Name: "podFoo",
|
Name: "podFoo",
|
||||||
Namespace: "nsFoo",
|
Namespace: "nsFoo",
|
||||||
Containers: []*kubecontainer.Container{
|
Containers: []*kubecontainer.Container{
|
||||||
{Name: "containerFoo",
|
{Name: "containerFoo",
|
||||||
ID: containerID,
|
ID: containerID,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
}},
|
||||||
}},
|
}
|
||||||
}
|
cmd := []string{"ls"}
|
||||||
cmd := []string{"ls"}
|
actualOutput, err := kubelet.RunInContainer("podFoo_nsFoo", "", "containerFoo", cmd)
|
||||||
_, err := kubelet.RunInContainer("podFoo_nsFoo", "", "containerFoo", cmd)
|
if fakeCommandRunner.ID != containerID {
|
||||||
if fakeCommandRunner.ID != containerID {
|
t.Errorf("(testError=%v) unexpected Name: %s", testError, fakeCommandRunner.ID)
|
||||||
t.Errorf("unexpected Name: %s", fakeCommandRunner.ID)
|
}
|
||||||
}
|
if !reflect.DeepEqual(fakeCommandRunner.Cmd, cmd) {
|
||||||
if !reflect.DeepEqual(fakeCommandRunner.Cmd, cmd) {
|
t.Errorf("(testError=%v) unexpected command: %s", testError, fakeCommandRunner.Cmd)
|
||||||
t.Errorf("unexpected command: %s", fakeCommandRunner.Cmd)
|
}
|
||||||
}
|
// this isn't 100% foolproof as a bug in a real ContainerCommandRunner where it fails to copy to stdout/stderr wouldn't be caught by this test
|
||||||
if err != nil {
|
if "foobar" != string(actualOutput) {
|
||||||
t.Errorf("unexpected error: %v", err)
|
t.Errorf("(testError=%v) unexpected output %q", testError, actualOutput)
|
||||||
|
}
|
||||||
|
if e, a := fmt.Sprintf("%v", testError), fmt.Sprintf("%v", err); e != a {
|
||||||
|
t.Errorf("(testError=%v) error: expected %s, got %s", testError, e, a)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -223,6 +223,8 @@ func formatURL(scheme string, host string, port int, path string) *url.URL {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type execInContainer struct {
|
type execInContainer struct {
|
||||||
|
// run executes a command in a container. Combined stdout and stderr output is always returned. An
|
||||||
|
// error is returned if one occurred.
|
||||||
run func() ([]byte, error)
|
run func() ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -231,11 +233,10 @@ func (p *prober) newExecInContainer(container api.Container, containerID kubecon
|
|||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
output := ioutils.WriteCloserWrapper(&buffer)
|
output := ioutils.WriteCloserWrapper(&buffer)
|
||||||
err := p.runner.ExecInContainer(containerID, cmd, nil, output, output, false, nil)
|
err := p.runner.ExecInContainer(containerID, cmd, nil, output, output, false, nil)
|
||||||
if err != nil {
|
// Even if err is non-nil, there still may be output (e.g. the exec wrote to stdout or stderr but
|
||||||
return nil, err
|
// the command returned a nonzero exit code). Therefore, always return the output along with the
|
||||||
}
|
// error.
|
||||||
|
return buffer.Bytes(), err
|
||||||
return buffer.Bytes(), nil
|
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ package prober
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
@ -29,6 +30,7 @@ import (
|
|||||||
"k8s.io/kubernetes/pkg/kubelet/prober/results"
|
"k8s.io/kubernetes/pkg/kubelet/prober/results"
|
||||||
"k8s.io/kubernetes/pkg/probe"
|
"k8s.io/kubernetes/pkg/probe"
|
||||||
"k8s.io/kubernetes/pkg/util/intstr"
|
"k8s.io/kubernetes/pkg/util/intstr"
|
||||||
|
"k8s.io/kubernetes/pkg/util/term"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFormatURL(t *testing.T) {
|
func TestFormatURL(t *testing.T) {
|
||||||
@ -276,3 +278,93 @@ func TestProbe(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type fakeContainerCommandRunner struct {
|
||||||
|
// what to return
|
||||||
|
stdoutData string
|
||||||
|
stderrData string
|
||||||
|
err error
|
||||||
|
|
||||||
|
// actual values when invoked
|
||||||
|
containerID kubecontainer.ContainerID
|
||||||
|
cmd []string
|
||||||
|
stdin io.Reader
|
||||||
|
tty bool
|
||||||
|
resize <-chan term.Size
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ kubecontainer.ContainerCommandRunner = &fakeContainerCommandRunner{}
|
||||||
|
|
||||||
|
func (f *fakeContainerCommandRunner) ExecInContainer(containerID kubecontainer.ContainerID, cmd []string, stdin io.Reader, stdout, stderr io.WriteCloser, tty bool, resize <-chan term.Size) error {
|
||||||
|
// record invoked values
|
||||||
|
f.containerID = containerID
|
||||||
|
f.cmd = cmd
|
||||||
|
f.stdin = stdin
|
||||||
|
f.tty = tty
|
||||||
|
f.resize = resize
|
||||||
|
|
||||||
|
fmt.Fprint(stdout, f.stdoutData)
|
||||||
|
fmt.Fprint(stdout, f.stderrData)
|
||||||
|
|
||||||
|
return f.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeContainerCommandRunner) PortForward(pod *kubecontainer.Pod, port uint16, stream io.ReadWriteCloser) error {
|
||||||
|
panic("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewExecInContainer(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no error",
|
||||||
|
err: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "error - make sure we get output",
|
||||||
|
err: errors.New("bad"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
runner := &fakeContainerCommandRunner{
|
||||||
|
stdoutData: "foo",
|
||||||
|
stderrData: "bar",
|
||||||
|
err: test.err,
|
||||||
|
}
|
||||||
|
prober := &prober{
|
||||||
|
runner: runner,
|
||||||
|
}
|
||||||
|
|
||||||
|
container := api.Container{}
|
||||||
|
containerID := kubecontainer.ContainerID{Type: "docker", ID: "containerID"}
|
||||||
|
cmd := []string{"/foo", "bar"}
|
||||||
|
exec := prober.newExecInContainer(container, containerID, cmd)
|
||||||
|
|
||||||
|
actualOutput, err := exec.CombinedOutput()
|
||||||
|
if e, a := containerID, runner.containerID; e != a {
|
||||||
|
t.Errorf("%s: container id: expected %v, got %v", test.name, e, a)
|
||||||
|
}
|
||||||
|
if e, a := cmd, runner.cmd; !reflect.DeepEqual(e, a) {
|
||||||
|
t.Errorf("%s: cmd: expected %v, got %v", test.name, e, a)
|
||||||
|
}
|
||||||
|
if runner.stdin != nil {
|
||||||
|
t.Errorf("%s: stdin: expected nil, got %v", test.name, runner.stdin)
|
||||||
|
}
|
||||||
|
if runner.tty {
|
||||||
|
t.Errorf("%s: tty: expected false", test.name)
|
||||||
|
}
|
||||||
|
if runner.resize != nil {
|
||||||
|
t.Errorf("%s: resize chan: expected nil, got %v", test.name, runner.resize)
|
||||||
|
}
|
||||||
|
// this isn't 100% foolproof as a bug in a real ContainerCommandRunner where it fails to copy to stdout/stderr wouldn't be caught by this test
|
||||||
|
if e, a := "foobar", string(actualOutput); e != a {
|
||||||
|
t.Errorf("%s: output: expected %q, got %q", test.name, e, a)
|
||||||
|
}
|
||||||
|
if e, a := fmt.Sprintf("%v", test.err), fmt.Sprintf("%v", err); e != a {
|
||||||
|
t.Errorf("%s: error: expected %s, got %s", test.name, e, a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user