Redirect command Stdout & Stderr for command_runner

CombinedOutput function return after command complete.
When we run a long run command, such as continuously get
new log entries, it failed to get run results.

Fixes: https://github.com/kubernetes/minikube/issues/2447
pull/2517/head
Chen Li 2018-01-23 15:53:22 +08:00 committed by dlorenc
parent 0e674ef3fa
commit 82ea016de7
10 changed files with 109 additions and 34 deletions

View File

@ -50,12 +50,11 @@ var logsCmd = &cobra.Command{
glog.Exitf("Error getting cluster bootstrapper: %s", err)
}
s, err := clusterBootstrapper.GetClusterLogs(follow)
err = clusterBootstrapper.GetClusterLogsTo(follow, os.Stdout)
if err != nil {
log.Println("Error getting machine logs:", err)
cmdUtil.MaybeReportErrorAndExit(err)
}
fmt.Fprintln(os.Stdout, s)
},
}

0
cmd/minikube/cmd/root.go Executable file → Normal file
View File

View File

@ -17,6 +17,7 @@ limitations under the License.
package bootstrapper
import (
"io"
"net"
"k8s.io/minikube/pkg/minikube/constants"
@ -28,7 +29,7 @@ type Bootstrapper interface {
StartCluster(KubernetesConfig) error
UpdateCluster(KubernetesConfig) error
RestartCluster(KubernetesConfig) error
GetClusterLogs(follow bool) (string, error)
GetClusterLogsTo(follow bool, out io.Writer) error
SetupCerts(cfg KubernetesConfig) error
GetClusterStatus() (string, error)
}

View File

@ -18,6 +18,7 @@ package bootstrapper
import (
"fmt"
"io"
"path/filepath"
"k8s.io/minikube/pkg/minikube/assets"
@ -28,6 +29,19 @@ type CommandRunner interface {
// Run starts the specified command and waits for it to complete.
Run(cmd string) error
// CombinedOutputTo runs the command and stores both command
// output and error to out. A typical usage is:
//
// var b bytes.Buffer
// CombinedOutput(cmd, &b)
// fmt.Println(b.Bytes())
//
// Or, you can set out to os.Stdout, the command output and
// error would show on your terminal immediately before you
// cmd exit. This is useful for a long run command such as
// continuously print running logs.
CombinedOutputTo(cmd string, out io.Writer) error
// CombinedOutput runs the command and returns its combined standard
// output and standard error.
CombinedOutput(cmd string) (string, error)

View File

@ -17,6 +17,7 @@ limitations under the License.
package bootstrapper
import (
"bytes"
"io"
"os"
"os/exec"
@ -43,16 +44,31 @@ func (*ExecRunner) Run(cmd string) error {
return nil
}
// CombinedOutput runs the command in a bash shell and returns its
// combined standard output and standard error.
func (*ExecRunner) CombinedOutput(cmd string) (string, error) {
// CombinedOutputTo runs the command and stores both command
// output and error to out.
func (*ExecRunner) CombinedOutputTo(cmd string, out io.Writer) error {
glog.Infoln("Run with output:", cmd)
c := exec.Command("/bin/bash", "-c", cmd)
out, err := c.CombinedOutput()
c.Stdout = out
c.Stderr = out
err := c.Run()
if err != nil {
return "", errors.Wrapf(err, "running command: %s\n output: %s", cmd, out)
return errors.Wrapf(err, "running command: %s\n.", cmd)
}
return string(out), nil
return nil
}
// CombinedOutput runs the command in a bash shell and returns its
// combined standard output and standard error.
func (e *ExecRunner) CombinedOutput(cmd string) (string, error) {
var b bytes.Buffer
err := e.CombinedOutputTo(cmd, &b)
if err != nil {
return "", errors.Wrapf(err, "running command: %s\n output: %s", cmd, b.Bytes())
}
return b.String(), nil
}
// Copy copies a file and its permissions

View File

@ -49,6 +49,21 @@ func (f *FakeCommandRunner) Run(cmd string) error {
return err
}
// CombinedOutputTo runs the command and stores both command
// output and error to out.
func (f *FakeCommandRunner) CombinedOutputTo(cmd string, out io.Writer) error {
value, ok := f.cmdMap.Load(cmd)
if !ok {
return fmt.Errorf("unavailable command: %s", cmd)
}
_, err := fmt.Fprint(out, value)
if err != nil {
return err
}
return nil
}
// CombinedOutput returns the set output for a given command text.
func (f *FakeCommandRunner) CombinedOutput(cmd string) (string, error) {
out, ok := f.cmdMap.Load(cmd)

View File

@ -20,6 +20,7 @@ import (
"bytes"
"crypto"
"fmt"
"io"
"os"
"path"
"strings"
@ -81,7 +82,7 @@ func (k *KubeadmBootstrapper) GetClusterStatus() (string, error) {
// TODO(r2d4): Should this aggregate all the logs from the control plane?
// Maybe subcommands for each component? minikube logs apiserver?
func (k *KubeadmBootstrapper) GetClusterLogs(follow bool) (string, error) {
func (k *KubeadmBootstrapper) GetClusterLogsTo(follow bool, out io.Writer) error {
var flags []string
if follow {
flags = append(flags, "-f")
@ -89,17 +90,18 @@ func (k *KubeadmBootstrapper) GetClusterLogs(follow bool) (string, error) {
logsCommand := fmt.Sprintf("sudo journalctl %s -u kubelet", strings.Join(flags, " "))
if follow {
if err := k.c.Run(logsCommand); err != nil {
return "", errors.Wrap(err, "getting shell")
if err := k.c.CombinedOutputTo(logsCommand, out); err != nil {
return errors.Wrap(err, "getting cluster logs")
}
}
} else {
logs, err := k.c.CombinedOutput(logsCommand)
if err != nil {
return "", errors.Wrap(err, "getting cluster logs")
logs, err := k.c.CombinedOutput(logsCommand)
if err != nil {
return errors.Wrap(err, "getting cluster logs")
}
fmt.Fprint(out, logs)
}
return logs, nil
return nil
}
func (k *KubeadmBootstrapper) StartCluster(k8s bootstrapper.KubernetesConfig) error {

View File

@ -18,6 +18,7 @@ package localkube
import (
"fmt"
"io"
"strings"
"k8s.io/minikube/pkg/minikube/assets"
@ -57,19 +58,27 @@ func NewLocalkubeBootstrapper(api libmachine.API) (*LocalkubeBootstrapper, error
}, nil
}
// GetClusterLogs If follow is specified, it will tail the logs
func (lk *LocalkubeBootstrapper) GetClusterLogs(follow bool) (string, error) {
// GetClusterLogs
// If follow is specified, it will tail the logs
func (lk *LocalkubeBootstrapper) GetClusterLogsTo(follow bool, out io.Writer) error {
logsCommand, err := GetLogsCommand(follow)
if err != nil {
return "", errors.Wrap(err, "Error getting logs command")
return errors.Wrap(err, "Error getting logs command")
}
logs, err := lk.cmd.CombinedOutput(logsCommand)
if err != nil {
return "", errors.Wrap(err, "getting cluster logs")
if follow {
err = lk.cmd.CombinedOutputTo(logsCommand, out)
if err != nil {
return errors.Wrap(err, "getting cluster logs")
}
} else {
logs, err := lk.cmd.CombinedOutput(logsCommand)
if err != nil {
return errors.Wrap(err, "getting cluster logs")
}
fmt.Fprint(out, logs)
}
return logs, nil
return nil
}
// GetClusterStatus gets the status of localkube from the host VM.

View File

@ -17,6 +17,7 @@ limitations under the License.
package localkube
import (
"bytes"
"testing"
"k8s.io/minikube/pkg/minikube/bootstrapper"
@ -200,13 +201,14 @@ func TestGetHostLogs(t *testing.T) {
},
}
var b bytes.Buffer
for _, test := range cases {
t.Run(test.description, func(t *testing.T) {
t.Parallel()
f := bootstrapper.NewFakeCommandRunner()
f.SetCommandToOutput(test.logsCmdMap)
l := LocalkubeBootstrapper{f}
_, err := l.GetClusterLogs(test.follow)
err := l.GetClusterLogsTo(test.follow, &b)
if err != nil && !test.shouldErr {
t.Errorf("Error getting localkube logs: %s", err)
return

View File

@ -17,6 +17,7 @@ limitations under the License.
package bootstrapper
import (
"bytes"
"fmt"
"io"
"path"
@ -63,20 +64,36 @@ func (s *SSHRunner) Run(cmd string) error {
return sess.Run(cmd)
}
// CombinedOutput runs the command on the remote and returns its combined
// standard output and standard error.
func (s *SSHRunner) CombinedOutput(cmd string) (string, error) {
// CombinedOutputTo runs the command and stores both command
// output and error to out.
func (s *SSHRunner) CombinedOutputTo(cmd string, out io.Writer) error {
glog.Infoln("Run with output:", cmd)
sess, err := s.c.NewSession()
if err != nil {
return "", errors.Wrap(err, "getting ssh session")
return errors.Wrap(err, "getting ssh session")
}
defer sess.Close()
out, err := sess.CombinedOutput(cmd)
sess.Stdout = out
sess.Stderr = out
err = sess.Run(cmd)
if err != nil {
return "", errors.Wrapf(err, "running command: %s\n output: %s", cmd, out)
return errors.Wrapf(err, "running command: %s\n.", cmd)
}
return string(out), nil
return nil
}
// CombinedOutput runs the command on the remote and returns its combined
// standard output and standard error.
func (s *SSHRunner) CombinedOutput(cmd string) (string, error) {
var b bytes.Buffer
err := s.CombinedOutputTo(cmd, &b)
if err != nil {
return "", errors.Wrapf(err, "running command: %s\n output: %s", cmd, b.Bytes())
}
return b.String(), nil
}
// Copy copies a file to the remote over SSH.