Support multi-container pod for "kubectl logs"

kubectl logs -l will print logs for pods with the same label, however
it doesn't support pods with multi containers. This change adds support
to it with --all-containers.

Ussage:
$ kubectl logs my-pod --all-containers
$ kubectl logs -laa=bb --all-containers
$ kubectl logs my-pod my-container --all-containers (err: --all-containers=true should not be specifiled with container name my-container)
This commit is contained in:
Cao Shufeng 2017-05-03 18:32:15 +08:00
parent 7d4bf1c338
commit a06f0077bd
2 changed files with 52 additions and 7 deletions

View File

@ -18,6 +18,7 @@ package cmd
import ( import (
"errors" "errors"
"fmt"
"io" "io"
"math" "math"
"os" "os"
@ -41,8 +42,11 @@ var (
# Return snapshot logs from pod nginx with only one container # Return snapshot logs from pod nginx with only one container
kubectl logs nginx kubectl logs nginx
# Return snapshot logs for the pods defined by label app=nginx # Return snapshot logs from pod nginx with multi containers
kubectl logs -lapp=nginx kubectl logs nginx --all-containers=true
# Return snapshot logs from all containers in pods defined by label app=nginx
kubectl logs -lapp=nginx --all-containers=true
# Return snapshot of previous terminated ruby container logs from pod web-1 # Return snapshot of previous terminated ruby container logs from pod web-1
kubectl logs -p -c ruby web-1 kubectl logs -p -c ruby web-1
@ -72,6 +76,7 @@ const (
type LogsOptions struct { type LogsOptions struct {
Namespace string Namespace string
ResourceArg string ResourceArg string
AllContainers bool
Options runtime.Object Options runtime.Object
Mapper meta.RESTMapper Mapper meta.RESTMapper
@ -106,6 +111,7 @@ func NewCmdLogs(f cmdutil.Factory, out, errOut io.Writer) *cobra.Command {
}, },
Aliases: []string{"log"}, Aliases: []string{"log"},
} }
cmd.Flags().Bool("all-containers", false, "Get all containers's logs in the pod(s).")
cmd.Flags().BoolP("follow", "f", false, "Specify if the logs should be streamed.") cmd.Flags().BoolP("follow", "f", false, "Specify if the logs should be streamed.")
cmd.Flags().Bool("timestamps", false, "Include timestamps on each line in the log output") cmd.Flags().Bool("timestamps", false, "Include timestamps on each line in the log output")
cmd.Flags().Int64("limit-bytes", 0, "Maximum bytes of logs to return. Defaults to no limit.") cmd.Flags().Int64("limit-bytes", 0, "Maximum bytes of logs to return. Defaults to no limit.")
@ -125,6 +131,7 @@ func NewCmdLogs(f cmdutil.Factory, out, errOut io.Writer) *cobra.Command {
func (o *LogsOptions) Complete(f cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []string) error { func (o *LogsOptions) Complete(f cmdutil.Factory, out io.Writer, cmd *cobra.Command, args []string) error {
containerName := cmdutil.GetFlagString(cmd, "container") containerName := cmdutil.GetFlagString(cmd, "container")
selector := cmdutil.GetFlagString(cmd, "selector") selector := cmdutil.GetFlagString(cmd, "selector")
o.AllContainers = cmdutil.GetFlagBool(cmd, "all-containers")
switch len(args) { switch len(args) {
case 0: case 0:
if len(selector) == 0 { if len(selector) == 0 {
@ -221,6 +228,9 @@ func (o LogsOptions) Validate() error {
if !ok { if !ok {
return errors.New("unexpected logs options object") return errors.New("unexpected logs options object")
} }
if o.AllContainers && len(logsOptions.Container) > 0 {
return fmt.Errorf("--all-containers=true should not be specifiled with container name %s", logsOptions.Container)
}
if errs := validation.ValidatePodLogOptions(logsOptions); len(errs) > 0 { if errs := validation.ValidatePodLogOptions(logsOptions); len(errs) > 0 {
return errs.ToAggregate() return errs.ToAggregate()
} }
@ -233,16 +243,40 @@ func (o LogsOptions) RunLogs() error {
switch t := o.Object.(type) { switch t := o.Object.(type) {
case *api.PodList: case *api.PodList:
for _, p := range t.Items { for _, p := range t.Items {
if err := o.getLogs(&p); err != nil { if err := o.getPodLogs(&p); err != nil {
return err return err
} }
} }
return nil return nil
case *api.Pod:
return o.getPodLogs(t)
default: default:
return o.getLogs(o.Object) return o.getLogs(o.Object)
} }
} }
// getPodLogs checks whether o.AllContainers is set to true.
// If so, it retrives all containers' log in the pod.
func (o LogsOptions) getPodLogs(pod *api.Pod) error {
if !o.AllContainers {
return o.getLogs(pod)
}
for _, c := range pod.Spec.InitContainers {
o.Options.(*api.PodLogOptions).Container = c.Name
if err := o.getLogs(pod); err != nil {
return err
}
}
for _, c := range pod.Spec.Containers {
o.Options.(*api.PodLogOptions).Container = c.Name
if err := o.getLogs(pod); err != nil {
return err
}
}
return nil
}
func (o LogsOptions) getLogs(obj runtime.Object) error { func (o LogsOptions) getLogs(obj runtime.Object) error {
req, err := o.LogsForObject(obj, o.Options, o.GetPodTimeout) req, err := o.LogsForObject(obj, o.Options, o.GetPodTimeout)
if err != nil { if err != nil {

View File

@ -109,28 +109,39 @@ func TestValidateLogFlags(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
flags map[string]string flags map[string]string
args []string
expected string expected string
}{ }{
{ {
name: "since & since-time", name: "since & since-time",
flags: map[string]string{"since": "1h", "since-time": "2006-01-02T15:04:05Z"}, flags: map[string]string{"since": "1h", "since-time": "2006-01-02T15:04:05Z"},
args: []string{"foo"},
expected: "at most one of `sinceTime` or `sinceSeconds` may be specified", expected: "at most one of `sinceTime` or `sinceSeconds` may be specified",
}, },
{ {
name: "negative since-time", name: "negative since-time",
flags: map[string]string{"since": "-1s"}, flags: map[string]string{"since": "-1s"},
args: []string{"foo"},
expected: "must be greater than 0", expected: "must be greater than 0",
}, },
{ {
name: "negative limit-bytes", name: "negative limit-bytes",
flags: map[string]string{"limit-bytes": "-100"}, flags: map[string]string{"limit-bytes": "-100"},
args: []string{"foo"},
expected: "must be greater than 0", expected: "must be greater than 0",
}, },
{ {
name: "negative tail", name: "negative tail",
flags: map[string]string{"tail": "-100"}, flags: map[string]string{"tail": "-100"},
args: []string{"foo"},
expected: "must be greater than or equal to 0", expected: "must be greater than or equal to 0",
}, },
{
name: "container name combined with --all-containers",
flags: map[string]string{"all-containers": "true"},
args: []string{"my-pod", "my-container"},
expected: "--all-containers=true should not be specifiled with container",
},
} }
for _, test := range tests { for _, test := range tests {
buf := bytes.NewBuffer([]byte{}) buf := bytes.NewBuffer([]byte{})
@ -146,7 +157,7 @@ func TestValidateLogFlags(t *testing.T) {
o.Complete(f, os.Stdout, cmd, args) o.Complete(f, os.Stdout, cmd, args)
out = o.Validate().Error() out = o.Validate().Error()
} }
cmd.Run(cmd, []string{"foo"}) cmd.Run(cmd, test.args)
if !strings.Contains(out, test.expected) { if !strings.Contains(out, test.expected) {
t.Errorf("%s: expected to find:\n\t%s\nfound:\n\t%s\n", test.name, test.expected, out) t.Errorf("%s: expected to find:\n\t%s\nfound:\n\t%s\n", test.name, test.expected, out)