mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-03 09:22:44 +00:00
feat: add all-pods log flag to kubectl
Signed-off-by: Case Wylie <cmwylie19@defenseunicorns.com>
This commit is contained in:
parent
1308e661e0
commit
6db859eb5d
@ -99,6 +99,7 @@ type LogsOptions struct {
|
|||||||
Namespace string
|
Namespace string
|
||||||
ResourceArg string
|
ResourceArg string
|
||||||
AllContainers bool
|
AllContainers bool
|
||||||
|
AllPods bool
|
||||||
Options runtime.Object
|
Options runtime.Object
|
||||||
Resources []string
|
Resources []string
|
||||||
|
|
||||||
@ -126,6 +127,7 @@ type LogsOptions struct {
|
|||||||
GetPodTimeout time.Duration
|
GetPodTimeout time.Duration
|
||||||
RESTClientGetter genericclioptions.RESTClientGetter
|
RESTClientGetter genericclioptions.RESTClientGetter
|
||||||
LogsForObject polymorphichelpers.LogsForObjectFunc
|
LogsForObject polymorphichelpers.LogsForObjectFunc
|
||||||
|
AllPodLogsForObject polymorphichelpers.AllPodLogsForObjectFunc
|
||||||
|
|
||||||
genericiooptions.IOStreams
|
genericiooptions.IOStreams
|
||||||
|
|
||||||
@ -134,10 +136,9 @@ type LogsOptions struct {
|
|||||||
containerNameFromRefSpecRegexp *regexp.Regexp
|
containerNameFromRefSpecRegexp *regexp.Regexp
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLogsOptions(streams genericiooptions.IOStreams, allContainers bool) *LogsOptions {
|
func NewLogsOptions(streams genericiooptions.IOStreams) *LogsOptions {
|
||||||
return &LogsOptions{
|
return &LogsOptions{
|
||||||
IOStreams: streams,
|
IOStreams: streams,
|
||||||
AllContainers: allContainers,
|
|
||||||
Tail: -1,
|
Tail: -1,
|
||||||
MaxFollowConcurrency: 5,
|
MaxFollowConcurrency: 5,
|
||||||
|
|
||||||
@ -147,7 +148,7 @@ func NewLogsOptions(streams genericiooptions.IOStreams, allContainers bool) *Log
|
|||||||
|
|
||||||
// NewCmdLogs creates a new pod logs command
|
// NewCmdLogs creates a new pod logs command
|
||||||
func NewCmdLogs(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
|
func NewCmdLogs(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
|
||||||
o := NewLogsOptions(streams, false)
|
o := NewLogsOptions(streams)
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: logsUsageStr,
|
Use: logsUsageStr,
|
||||||
@ -167,6 +168,7 @@ func NewCmdLogs(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Co
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (o *LogsOptions) AddFlags(cmd *cobra.Command) {
|
func (o *LogsOptions) AddFlags(cmd *cobra.Command) {
|
||||||
|
cmd.Flags().BoolVar(&o.AllPods, "all-pods", o.AllPods, "Get logs from all pod(s). Sets prefix to true.")
|
||||||
cmd.Flags().BoolVar(&o.AllContainers, "all-containers", o.AllContainers, "Get all containers' logs in the pod(s).")
|
cmd.Flags().BoolVar(&o.AllContainers, "all-containers", o.AllContainers, "Get all containers' logs in the pod(s).")
|
||||||
cmd.Flags().BoolVarP(&o.Follow, "follow", "f", o.Follow, "Specify if the logs should be streamed.")
|
cmd.Flags().BoolVarP(&o.Follow, "follow", "f", o.Follow, "Specify if the logs should be streamed.")
|
||||||
cmd.Flags().BoolVar(&o.Timestamps, "timestamps", o.Timestamps, "Include timestamps on each line in the log output")
|
cmd.Flags().BoolVar(&o.Timestamps, "timestamps", o.Timestamps, "Include timestamps on each line in the log output")
|
||||||
@ -243,6 +245,11 @@ func (o *LogsOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []str
|
|||||||
default:
|
default:
|
||||||
return cmdutil.UsageErrorf(cmd, "%s", logsUsageErrStr)
|
return cmdutil.UsageErrorf(cmd, "%s", logsUsageErrStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if o.AllPods {
|
||||||
|
o.Prefix = true
|
||||||
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace()
|
o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -263,6 +270,7 @@ func (o *LogsOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []str
|
|||||||
|
|
||||||
o.RESTClientGetter = f
|
o.RESTClientGetter = f
|
||||||
o.LogsForObject = polymorphichelpers.LogsForObjectFn
|
o.LogsForObject = polymorphichelpers.LogsForObjectFn
|
||||||
|
o.AllPodLogsForObject = polymorphichelpers.AllPodLogsForObjectFn
|
||||||
|
|
||||||
if o.Object == nil {
|
if o.Object == nil {
|
||||||
builder := f.NewBuilder().
|
builder := f.NewBuilder().
|
||||||
@ -328,7 +336,13 @@ func (o LogsOptions) Validate() error {
|
|||||||
|
|
||||||
// RunLogs retrieves a pod log
|
// RunLogs retrieves a pod log
|
||||||
func (o LogsOptions) RunLogs() error {
|
func (o LogsOptions) RunLogs() error {
|
||||||
requests, err := o.LogsForObject(o.RESTClientGetter, o.Object, o.Options, o.GetPodTimeout, o.AllContainers)
|
var requests map[corev1.ObjectReference]rest.ResponseWrapper
|
||||||
|
var err error
|
||||||
|
if o.AllPods {
|
||||||
|
requests, err = o.AllPodLogsForObject(o.RESTClientGetter, o.Object, o.Options, o.GetPodTimeout, o.AllContainers)
|
||||||
|
} else {
|
||||||
|
requests, err = o.LogsForObject(o.RESTClientGetter, o.Object, o.Options, o.GetPodTimeout, o.AllContainers)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,7 @@ import (
|
|||||||
"testing/iotest"
|
"testing/iotest"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
@ -62,7 +63,7 @@ func TestLog(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
o := NewLogsOptions(streams, false)
|
o := NewLogsOptions(streams)
|
||||||
o.LogsForObject = mock.mockLogsForObject
|
o.LogsForObject = mock.mockLogsForObject
|
||||||
o.ConsumeRequestFn = mock.mockConsumeRequest
|
o.ConsumeRequestFn = mock.mockConsumeRequest
|
||||||
|
|
||||||
@ -83,7 +84,7 @@ func TestLog(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
o := NewLogsOptions(streams, false)
|
o := NewLogsOptions(streams)
|
||||||
o.LogsForObject = mock.mockLogsForObject
|
o.LogsForObject = mock.mockLogsForObject
|
||||||
o.ConsumeRequestFn = mock.mockConsumeRequest
|
o.ConsumeRequestFn = mock.mockConsumeRequest
|
||||||
o.Prefix = true
|
o.Prefix = true
|
||||||
@ -92,6 +93,32 @@ func TestLog(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedOutSubstrings: []string{"[pod/test-pod/test-container] test log content\n"},
|
expectedOutSubstrings: []string{"[pod/test-pod/test-container] test log content\n"},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "stateful set logs with all pods",
|
||||||
|
opts: func(streams genericiooptions.IOStreams) *LogsOptions {
|
||||||
|
mock := &logTestMock{
|
||||||
|
logsForObjectRequests: map[corev1.ObjectReference]restclient.ResponseWrapper{
|
||||||
|
{
|
||||||
|
Kind: "Pod",
|
||||||
|
Name: "test-sts-0",
|
||||||
|
FieldPath: "spec.containers{test-container}",
|
||||||
|
}: &responseWrapperMock{data: strings.NewReader("test log content for pod test-sts-0\n")},
|
||||||
|
{
|
||||||
|
Kind: "Pod",
|
||||||
|
Name: "test-sts-1",
|
||||||
|
FieldPath: "spec.containers{test-container}",
|
||||||
|
}: &responseWrapperMock{data: strings.NewReader("test log content for pod test-sts-1\n")},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
o := NewLogsOptions(streams)
|
||||||
|
o.LogsForObject = mock.mockLogsForObject
|
||||||
|
o.ConsumeRequestFn = mock.mockConsumeRequest
|
||||||
|
o.Prefix = true
|
||||||
|
return o
|
||||||
|
},
|
||||||
|
expectedOutSubstrings: []string{"[pod/test-sts-0/test-container] test log content for pod test-sts-0\n[pod/test-sts-1/test-container] test log content for pod test-sts-1\n"},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "pod logs with prefix: init container",
|
name: "pod logs with prefix: init container",
|
||||||
opts: func(streams genericiooptions.IOStreams) *LogsOptions {
|
opts: func(streams genericiooptions.IOStreams) *LogsOptions {
|
||||||
@ -105,7 +132,7 @@ func TestLog(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
o := NewLogsOptions(streams, false)
|
o := NewLogsOptions(streams)
|
||||||
o.LogsForObject = mock.mockLogsForObject
|
o.LogsForObject = mock.mockLogsForObject
|
||||||
o.ConsumeRequestFn = mock.mockConsumeRequest
|
o.ConsumeRequestFn = mock.mockConsumeRequest
|
||||||
o.Prefix = true
|
o.Prefix = true
|
||||||
@ -127,7 +154,7 @@ func TestLog(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
o := NewLogsOptions(streams, false)
|
o := NewLogsOptions(streams)
|
||||||
o.LogsForObject = mock.mockLogsForObject
|
o.LogsForObject = mock.mockLogsForObject
|
||||||
o.ConsumeRequestFn = mock.mockConsumeRequest
|
o.ConsumeRequestFn = mock.mockConsumeRequest
|
||||||
o.Prefix = true
|
o.Prefix = true
|
||||||
@ -159,7 +186,7 @@ func TestLog(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
o := NewLogsOptions(streams, false)
|
o := NewLogsOptions(streams)
|
||||||
o.LogsForObject = mock.mockLogsForObject
|
o.LogsForObject = mock.mockLogsForObject
|
||||||
o.ConsumeRequestFn = mock.mockConsumeRequest
|
o.ConsumeRequestFn = mock.mockConsumeRequest
|
||||||
return o
|
return o
|
||||||
@ -196,7 +223,7 @@ func TestLog(t *testing.T) {
|
|||||||
}
|
}
|
||||||
wg.Add(3)
|
wg.Add(3)
|
||||||
|
|
||||||
o := NewLogsOptions(streams, false)
|
o := NewLogsOptions(streams)
|
||||||
o.LogsForObject = mock.mockLogsForObject
|
o.LogsForObject = mock.mockLogsForObject
|
||||||
o.ConsumeRequestFn = mock.mockConsumeRequest
|
o.ConsumeRequestFn = mock.mockConsumeRequest
|
||||||
o.Follow = true
|
o.Follow = true
|
||||||
@ -234,7 +261,7 @@ func TestLog(t *testing.T) {
|
|||||||
}
|
}
|
||||||
wg.Add(3)
|
wg.Add(3)
|
||||||
|
|
||||||
o := NewLogsOptions(streams, false)
|
o := NewLogsOptions(streams)
|
||||||
o.LogsForObject = mock.mockLogsForObject
|
o.LogsForObject = mock.mockLogsForObject
|
||||||
o.ConsumeRequestFn = mock.mockConsumeRequest
|
o.ConsumeRequestFn = mock.mockConsumeRequest
|
||||||
o.MaxFollowConcurrency = 2
|
o.MaxFollowConcurrency = 2
|
||||||
@ -246,7 +273,7 @@ func TestLog(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "fail if LogsForObject fails",
|
name: "fail if LogsForObject fails",
|
||||||
opts: func(streams genericiooptions.IOStreams) *LogsOptions {
|
opts: func(streams genericiooptions.IOStreams) *LogsOptions {
|
||||||
o := NewLogsOptions(streams, false)
|
o := NewLogsOptions(streams)
|
||||||
o.LogsForObject = func(restClientGetter genericclioptions.RESTClientGetter, object, options runtime.Object, timeout time.Duration, allContainers bool) (map[corev1.ObjectReference]restclient.ResponseWrapper, error) {
|
o.LogsForObject = func(restClientGetter genericclioptions.RESTClientGetter, object, options runtime.Object, timeout time.Duration, allContainers bool) (map[corev1.ObjectReference]restclient.ResponseWrapper, error) {
|
||||||
return nil, errors.New("Error from the LogsForObject")
|
return nil, errors.New("Error from the LogsForObject")
|
||||||
}
|
}
|
||||||
@ -272,7 +299,7 @@ func TestLog(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
o := NewLogsOptions(streams, false)
|
o := NewLogsOptions(streams)
|
||||||
o.LogsForObject = mock.mockLogsForObject
|
o.LogsForObject = mock.mockLogsForObject
|
||||||
o.ConsumeRequestFn = func(req restclient.ResponseWrapper, out io.Writer) error {
|
o.ConsumeRequestFn = func(req restclient.ResponseWrapper, out io.Writer) error {
|
||||||
return errors.New("Error from the ConsumeRequestFn")
|
return errors.New("Error from the ConsumeRequestFn")
|
||||||
@ -307,7 +334,7 @@ func TestLog(t *testing.T) {
|
|||||||
}
|
}
|
||||||
wg.Add(3)
|
wg.Add(3)
|
||||||
|
|
||||||
o := NewLogsOptions(streams, false)
|
o := NewLogsOptions(streams)
|
||||||
o.LogsForObject = mock.mockLogsForObject
|
o.LogsForObject = mock.mockLogsForObject
|
||||||
o.ConsumeRequestFn = mock.mockConsumeRequest
|
o.ConsumeRequestFn = mock.mockConsumeRequest
|
||||||
o.Follow = true
|
o.Follow = true
|
||||||
@ -346,7 +373,7 @@ func TestLog(t *testing.T) {
|
|||||||
}
|
}
|
||||||
wg.Add(3)
|
wg.Add(3)
|
||||||
|
|
||||||
o := NewLogsOptions(streams, false)
|
o := NewLogsOptions(streams)
|
||||||
o.LogsForObject = mock.mockLogsForObject
|
o.LogsForObject = mock.mockLogsForObject
|
||||||
o.ConsumeRequestFn = func(req restclient.ResponseWrapper, out io.Writer) error {
|
o.ConsumeRequestFn = func(req restclient.ResponseWrapper, out io.Writer) error {
|
||||||
return errors.New("Error from the ConsumeRequestFn")
|
return errors.New("Error from the ConsumeRequestFn")
|
||||||
@ -369,7 +396,7 @@ func TestLog(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
o := NewLogsOptions(streams, false)
|
o := NewLogsOptions(streams)
|
||||||
o.LogsForObject = mock.mockLogsForObject
|
o.LogsForObject = mock.mockLogsForObject
|
||||||
o.ConsumeRequestFn = func(req restclient.ResponseWrapper, out io.Writer) error {
|
o.ConsumeRequestFn = func(req restclient.ResponseWrapper, out io.Writer) error {
|
||||||
return errors.New("Error from the ConsumeRequestFn")
|
return errors.New("Error from the ConsumeRequestFn")
|
||||||
@ -402,7 +429,7 @@ func TestLog(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
o := NewLogsOptions(streams, false)
|
o := NewLogsOptions(streams)
|
||||||
o.LogsForObject = mock.mockLogsForObject
|
o.LogsForObject = mock.mockLogsForObject
|
||||||
o.ConsumeRequestFn = mock.mockConsumeRequest
|
o.ConsumeRequestFn = mock.mockConsumeRequest
|
||||||
o.IgnoreLogErrors = true
|
o.IgnoreLogErrors = true
|
||||||
@ -432,7 +459,7 @@ func TestLog(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
o := NewLogsOptions(streams, false)
|
o := NewLogsOptions(streams)
|
||||||
o.LogsForObject = mock.mockLogsForObject
|
o.LogsForObject = mock.mockLogsForObject
|
||||||
o.ConsumeRequestFn = mock.mockConsumeRequest
|
o.ConsumeRequestFn = mock.mockConsumeRequest
|
||||||
return o
|
return o
|
||||||
@ -462,7 +489,7 @@ func TestLog(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
o := NewLogsOptions(streams, false)
|
o := NewLogsOptions(streams)
|
||||||
o.LogsForObject = mock.mockLogsForObject
|
o.LogsForObject = mock.mockLogsForObject
|
||||||
o.ConsumeRequestFn = mock.mockConsumeRequest
|
o.ConsumeRequestFn = mock.mockConsumeRequest
|
||||||
o.IgnoreLogErrors = true
|
o.IgnoreLogErrors = true
|
||||||
@ -493,7 +520,7 @@ func TestLog(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
o := NewLogsOptions(streams, false)
|
o := NewLogsOptions(streams)
|
||||||
o.LogsForObject = mock.mockLogsForObject
|
o.LogsForObject = mock.mockLogsForObject
|
||||||
o.ConsumeRequestFn = mock.mockConsumeRequest
|
o.ConsumeRequestFn = mock.mockConsumeRequest
|
||||||
o.Follow = true
|
o.Follow = true
|
||||||
@ -564,7 +591,7 @@ func TestValidateLogOptions(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "since & since-time",
|
name: "since & since-time",
|
||||||
opts: func(streams genericiooptions.IOStreams) *LogsOptions {
|
opts: func(streams genericiooptions.IOStreams) *LogsOptions {
|
||||||
o := NewLogsOptions(streams, false)
|
o := NewLogsOptions(streams)
|
||||||
o.SinceSeconds = time.Hour
|
o.SinceSeconds = time.Hour
|
||||||
o.SinceTime = "2006-01-02T15:04:05Z"
|
o.SinceTime = "2006-01-02T15:04:05Z"
|
||||||
|
|
||||||
@ -582,7 +609,7 @@ func TestValidateLogOptions(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "negative since-time",
|
name: "negative since-time",
|
||||||
opts: func(streams genericiooptions.IOStreams) *LogsOptions {
|
opts: func(streams genericiooptions.IOStreams) *LogsOptions {
|
||||||
o := NewLogsOptions(streams, false)
|
o := NewLogsOptions(streams)
|
||||||
o.SinceSeconds = -1 * time.Second
|
o.SinceSeconds = -1 * time.Second
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
@ -599,7 +626,7 @@ func TestValidateLogOptions(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "negative limit-bytes",
|
name: "negative limit-bytes",
|
||||||
opts: func(streams genericiooptions.IOStreams) *LogsOptions {
|
opts: func(streams genericiooptions.IOStreams) *LogsOptions {
|
||||||
o := NewLogsOptions(streams, false)
|
o := NewLogsOptions(streams)
|
||||||
o.LimitBytes = -100
|
o.LimitBytes = -100
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
@ -616,7 +643,7 @@ func TestValidateLogOptions(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "negative tail",
|
name: "negative tail",
|
||||||
opts: func(streams genericiooptions.IOStreams) *LogsOptions {
|
opts: func(streams genericiooptions.IOStreams) *LogsOptions {
|
||||||
o := NewLogsOptions(streams, false)
|
o := NewLogsOptions(streams)
|
||||||
o.Tail = -100
|
o.Tail = -100
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
@ -633,7 +660,8 @@ func TestValidateLogOptions(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "container name combined with --all-containers",
|
name: "container name combined with --all-containers",
|
||||||
opts: func(streams genericiooptions.IOStreams) *LogsOptions {
|
opts: func(streams genericiooptions.IOStreams) *LogsOptions {
|
||||||
o := NewLogsOptions(streams, true)
|
o := NewLogsOptions(streams)
|
||||||
|
o.AllContainers = true
|
||||||
o.Container = "my-container"
|
o.Container = "my-container"
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
@ -650,7 +678,7 @@ func TestValidateLogOptions(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "container name combined with second argument",
|
name: "container name combined with second argument",
|
||||||
opts: func(streams genericiooptions.IOStreams) *LogsOptions {
|
opts: func(streams genericiooptions.IOStreams) *LogsOptions {
|
||||||
o := NewLogsOptions(streams, false)
|
o := NewLogsOptions(streams)
|
||||||
o.Container = "my-container"
|
o.Container = "my-container"
|
||||||
o.ContainerNameSpecified = true
|
o.ContainerNameSpecified = true
|
||||||
|
|
||||||
@ -697,7 +725,7 @@ func TestLogComplete(t *testing.T) {
|
|||||||
name: "One args case",
|
name: "One args case",
|
||||||
args: []string{"foo"},
|
args: []string{"foo"},
|
||||||
opts: func(streams genericiooptions.IOStreams) *LogsOptions {
|
opts: func(streams genericiooptions.IOStreams) *LogsOptions {
|
||||||
o := NewLogsOptions(streams, false)
|
o := NewLogsOptions(streams)
|
||||||
o.Selector = "foo"
|
o.Selector = "foo"
|
||||||
return o
|
return o
|
||||||
},
|
},
|
||||||
@ -816,7 +844,7 @@ func TestNoResourceFoundMessage(t *testing.T) {
|
|||||||
|
|
||||||
streams, _, buf, errbuf := genericiooptions.NewTestIOStreams()
|
streams, _, buf, errbuf := genericiooptions.NewTestIOStreams()
|
||||||
cmd := NewCmdLogs(tf, streams)
|
cmd := NewCmdLogs(tf, streams)
|
||||||
o := NewLogsOptions(streams, false)
|
o := NewLogsOptions(streams)
|
||||||
o.Selector = "foo"
|
o.Selector = "foo"
|
||||||
err := o.Complete(tf, cmd, []string{})
|
err := o.Complete(tf, cmd, []string{})
|
||||||
|
|
||||||
@ -864,7 +892,7 @@ func TestNoPodInNamespaceFoundMessage(t *testing.T) {
|
|||||||
|
|
||||||
streams, _, _, _ := genericiooptions.NewTestIOStreams()
|
streams, _, _, _ := genericiooptions.NewTestIOStreams()
|
||||||
cmd := NewCmdLogs(tf, streams)
|
cmd := NewCmdLogs(tf, streams)
|
||||||
o := NewLogsOptions(streams, false)
|
o := NewLogsOptions(streams)
|
||||||
err := o.Complete(tf, cmd, []string{podName})
|
err := o.Complete(tf, cmd, []string{podName})
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@ -919,6 +947,13 @@ func (l *logTestMock) mockConsumeRequest(request restclient.ResponseWrapper, out
|
|||||||
|
|
||||||
func (l *logTestMock) mockLogsForObject(restClientGetter genericclioptions.RESTClientGetter, object, options runtime.Object, timeout time.Duration, allContainers bool) (map[corev1.ObjectReference]restclient.ResponseWrapper, error) {
|
func (l *logTestMock) mockLogsForObject(restClientGetter genericclioptions.RESTClientGetter, object, options runtime.Object, timeout time.Duration, allContainers bool) (map[corev1.ObjectReference]restclient.ResponseWrapper, error) {
|
||||||
switch object.(type) {
|
switch object.(type) {
|
||||||
|
case *appsv1.Deployment:
|
||||||
|
_, ok := options.(*corev1.PodLogOptions)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.New("provided options object is not a PodLogOptions")
|
||||||
|
}
|
||||||
|
|
||||||
|
return l.logsForObjectRequests, nil
|
||||||
case *corev1.Pod:
|
case *corev1.Pod:
|
||||||
_, ok := options.(*corev1.PodLogOptions)
|
_, ok := options.(*corev1.PodLogOptions)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -36,15 +36,15 @@ import (
|
|||||||
watchtools "k8s.io/client-go/tools/watch"
|
watchtools "k8s.io/client-go/tools/watch"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetFirstPod returns a pod matching the namespace and label selector
|
// GetPodList returns a PodList matching the namespace and label selector
|
||||||
// and the number of all pods that match the label selector.
|
func GetPodList(client coreclient.PodsGetter, namespace string, selector string, timeout time.Duration, sortBy func([]*corev1.Pod) sort.Interface) (*corev1.PodList, error) {
|
||||||
func GetFirstPod(client coreclient.PodsGetter, namespace string, selector string, timeout time.Duration, sortBy func([]*corev1.Pod) sort.Interface) (*corev1.Pod, int, error) {
|
|
||||||
options := metav1.ListOptions{LabelSelector: selector}
|
options := metav1.ListOptions{LabelSelector: selector}
|
||||||
|
|
||||||
podList, err := client.Pods(namespace).List(context.TODO(), options)
|
podList, err := client.Pods(namespace).List(context.TODO(), options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
pods := []*corev1.Pod{}
|
pods := []*corev1.Pod{}
|
||||||
for i := range podList.Items {
|
for i := range podList.Items {
|
||||||
pod := podList.Items[i]
|
pod := podList.Items[i]
|
||||||
@ -52,14 +52,17 @@ func GetFirstPod(client coreclient.PodsGetter, namespace string, selector string
|
|||||||
}
|
}
|
||||||
if len(pods) > 0 {
|
if len(pods) > 0 {
|
||||||
sort.Sort(sortBy(pods))
|
sort.Sort(sortBy(pods))
|
||||||
return pods[0], len(podList.Items), nil
|
for i, pod := range pods {
|
||||||
|
podList.Items[i] = *pod
|
||||||
|
}
|
||||||
|
return podList, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watch until we observe a pod
|
// Watch until we observe a pod
|
||||||
options.ResourceVersion = podList.ResourceVersion
|
options.ResourceVersion = podList.ResourceVersion
|
||||||
w, err := client.Pods(namespace).Watch(context.TODO(), options)
|
w, err := client.Pods(namespace).Watch(context.TODO(), options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer w.Stop()
|
defer w.Stop()
|
||||||
|
|
||||||
@ -70,14 +73,30 @@ func GetFirstPod(client coreclient.PodsGetter, namespace string, selector string
|
|||||||
ctx, cancel := watchtools.ContextWithOptionalTimeout(context.Background(), timeout)
|
ctx, cancel := watchtools.ContextWithOptionalTimeout(context.Background(), timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
event, err := watchtools.UntilWithoutRetry(ctx, w, condition)
|
event, err := watchtools.UntilWithoutRetry(ctx, w, condition)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
po, ok := event.Object.(*corev1.Pod)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("%#v is not a pod event", event)
|
||||||
|
} else {
|
||||||
|
podList.Items = append(podList.Items, *po)
|
||||||
|
}
|
||||||
|
return podList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFirstPod returns a pod matching the namespace and label selector
|
||||||
|
// and the number of all pods that match the label selector.
|
||||||
|
func GetFirstPod(client coreclient.PodsGetter, namespace string, selector string, timeout time.Duration, sortBy func([]*corev1.Pod) sort.Interface) (*corev1.Pod, int, error) {
|
||||||
|
|
||||||
|
podList, err := GetPodList(client, namespace, selector, timeout, sortBy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
pod, ok := event.Object.(*corev1.Pod)
|
|
||||||
if !ok {
|
return &podList.Items[0], len(podList.Items), nil
|
||||||
return nil, 0, fmt.Errorf("%#v is not a pod event", event)
|
|
||||||
}
|
|
||||||
return pod, 1, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SelectorsForObject returns the pod label selector for a given object
|
// SelectorsForObject returns the pod label selector for a given object
|
||||||
|
@ -32,6 +32,98 @@ import (
|
|||||||
"k8s.io/kubectl/pkg/util/podutils"
|
"k8s.io/kubectl/pkg/util/podutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestGetPodList(t *testing.T) {
|
||||||
|
labelSet := map[string]string{"test": "selector"}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
|
||||||
|
podList *corev1.PodList
|
||||||
|
watching []watch.Event
|
||||||
|
sortBy func([]*corev1.Pod) sort.Interface
|
||||||
|
|
||||||
|
expected *corev1.PodList
|
||||||
|
expectedNum int
|
||||||
|
expectedErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "kubectl logs - two ready pods",
|
||||||
|
podList: newPodList(2, -1, -1, labelSet),
|
||||||
|
sortBy: func(pods []*corev1.Pod) sort.Interface { return podutils.ByLogging(pods) },
|
||||||
|
expected: &corev1.PodList{
|
||||||
|
Items: []corev1.Pod{
|
||||||
|
{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "pod-1",
|
||||||
|
Namespace: metav1.NamespaceDefault,
|
||||||
|
CreationTimestamp: metav1.Date(2016, time.April, 1, 1, 0, 0, 0, time.UTC),
|
||||||
|
Labels: map[string]string{"test": "selector"},
|
||||||
|
},
|
||||||
|
Status: corev1.PodStatus{
|
||||||
|
Conditions: []corev1.PodCondition{
|
||||||
|
{
|
||||||
|
Status: corev1.ConditionTrue,
|
||||||
|
Type: corev1.PodReady,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "pod-2",
|
||||||
|
Namespace: metav1.NamespaceDefault,
|
||||||
|
CreationTimestamp: metav1.Date(2016, time.April, 1, 1, 0, 1, 0, time.UTC),
|
||||||
|
Labels: map[string]string{"test": "selector"},
|
||||||
|
},
|
||||||
|
Status: corev1.PodStatus{
|
||||||
|
Conditions: []corev1.PodCondition{
|
||||||
|
{
|
||||||
|
Status: corev1.ConditionTrue,
|
||||||
|
Type: corev1.PodReady,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedNum: 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range tests {
|
||||||
|
test := tests[i]
|
||||||
|
fake := fakeexternal.NewSimpleClientset(test.podList)
|
||||||
|
if len(test.watching) > 0 {
|
||||||
|
watcher := watch.NewFake()
|
||||||
|
for _, event := range test.watching {
|
||||||
|
switch event.Type {
|
||||||
|
case watch.Added:
|
||||||
|
go watcher.Add(event.Object)
|
||||||
|
case watch.Modified:
|
||||||
|
go watcher.Modify(event.Object)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fake.PrependWatchReactor("pods", testcore.DefaultWatchReactor(watcher, nil))
|
||||||
|
}
|
||||||
|
selector := labels.Set(labelSet).AsSelector()
|
||||||
|
podList, err := GetPodList(fake.CoreV1(), metav1.NamespaceDefault, selector.String(), 1*time.Minute, test.sortBy)
|
||||||
|
|
||||||
|
if !test.expectedErr && err != nil {
|
||||||
|
t.Errorf("%s: unexpected error: %v", test.name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if test.expectedErr && err == nil {
|
||||||
|
t.Errorf("%s: expected an error", test.name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if test.expectedNum != len(podList.Items) {
|
||||||
|
t.Errorf("%s: expected %d pods, got %d", test.name, test.expectedNum, len(podList.Items))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !apiequality.Semantic.DeepEqual(test.expected, podList) {
|
||||||
|
t.Errorf("%s:\nexpected podList:\n%#v\ngot:\n%#v\n\n", test.name, test.expected, podList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
func TestGetFirstPod(t *testing.T) {
|
func TestGetFirstPod(t *testing.T) {
|
||||||
labelSet := map[string]string{"test": "selector"}
|
labelSet := map[string]string{"test": "selector"}
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
@ -27,6 +27,12 @@ import (
|
|||||||
"k8s.io/client-go/rest"
|
"k8s.io/client-go/rest"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// AllPodLogsForObjectFunc is a function type that can tell you how to get logs for a runtime.object
|
||||||
|
type AllPodLogsForObjectFunc func(restClientGetter genericclioptions.RESTClientGetter, object, options runtime.Object, timeout time.Duration, allContainers bool) (map[v1.ObjectReference]rest.ResponseWrapper, error)
|
||||||
|
|
||||||
|
// AllPodLogsForObjectFn gives a way to easily override the function for unit testing if needed.
|
||||||
|
var AllPodLogsForObjectFn AllPodLogsForObjectFunc = allPodLogsForObject
|
||||||
|
|
||||||
// LogsForObjectFunc is a function type that can tell you how to get logs for a runtime.object
|
// LogsForObjectFunc is a function type that can tell you how to get logs for a runtime.object
|
||||||
type LogsForObjectFunc func(restClientGetter genericclioptions.RESTClientGetter, object, options runtime.Object, timeout time.Duration, allContainers bool) (map[v1.ObjectReference]rest.ResponseWrapper, error)
|
type LogsForObjectFunc func(restClientGetter genericclioptions.RESTClientGetter, object, options runtime.Object, timeout time.Duration, allContainers bool) (map[v1.ObjectReference]rest.ResponseWrapper, error)
|
||||||
|
|
||||||
|
@ -34,6 +34,19 @@ import (
|
|||||||
"k8s.io/kubectl/pkg/util/podutils"
|
"k8s.io/kubectl/pkg/util/podutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func allPodLogsForObject(restClientGetter genericclioptions.RESTClientGetter, object, options runtime.Object, timeout time.Duration, allContainers bool) (map[corev1.ObjectReference]rest.ResponseWrapper, error) {
|
||||||
|
clientConfig, err := restClientGetter.ToRESTConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
clientset, err := corev1client.NewForConfig(clientConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return logsForObjectWithClient(clientset, object, options, timeout, allContainers, true)
|
||||||
|
}
|
||||||
|
|
||||||
func logsForObject(restClientGetter genericclioptions.RESTClientGetter, object, options runtime.Object, timeout time.Duration, allContainers bool) (map[corev1.ObjectReference]rest.ResponseWrapper, error) {
|
func logsForObject(restClientGetter genericclioptions.RESTClientGetter, object, options runtime.Object, timeout time.Duration, allContainers bool) (map[corev1.ObjectReference]rest.ResponseWrapper, error) {
|
||||||
clientConfig, err := restClientGetter.ToRESTConfig()
|
clientConfig, err := restClientGetter.ToRESTConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -44,11 +57,11 @@ func logsForObject(restClientGetter genericclioptions.RESTClientGetter, object,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return logsForObjectWithClient(clientset, object, options, timeout, allContainers)
|
return logsForObjectWithClient(clientset, object, options, timeout, allContainers, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// this is split for easy test-ability
|
// this is split for easy test-ability
|
||||||
func logsForObjectWithClient(clientset corev1client.CoreV1Interface, object, options runtime.Object, timeout time.Duration, allContainers bool) (map[corev1.ObjectReference]rest.ResponseWrapper, error) {
|
func logsForObjectWithClient(clientset corev1client.CoreV1Interface, object, options runtime.Object, timeout time.Duration, allContainers bool, allPods bool) (map[corev1.ObjectReference]rest.ResponseWrapper, error) {
|
||||||
opts, ok := options.(*corev1.PodLogOptions)
|
opts, ok := options.(*corev1.PodLogOptions)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.New("provided options object is not a PodLogOptions")
|
return nil, errors.New("provided options object is not a PodLogOptions")
|
||||||
@ -58,7 +71,7 @@ func logsForObjectWithClient(clientset corev1client.CoreV1Interface, object, opt
|
|||||||
case *corev1.PodList:
|
case *corev1.PodList:
|
||||||
ret := make(map[corev1.ObjectReference]rest.ResponseWrapper)
|
ret := make(map[corev1.ObjectReference]rest.ResponseWrapper)
|
||||||
for i := range t.Items {
|
for i := range t.Items {
|
||||||
currRet, err := logsForObjectWithClient(clientset, &t.Items[i], options, timeout, allContainers)
|
currRet, err := logsForObjectWithClient(clientset, &t.Items[i], options, timeout, allContainers, allPods)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -95,9 +108,11 @@ func logsForObjectWithClient(clientset corev1client.CoreV1Interface, object, opt
|
|||||||
// Default to the first container name(aligning behavior with `kubectl exec').
|
// Default to the first container name(aligning behavior with `kubectl exec').
|
||||||
currOpts.Container = t.Spec.Containers[0].Name
|
currOpts.Container = t.Spec.Containers[0].Name
|
||||||
if len(t.Spec.Containers) > 1 || len(t.Spec.InitContainers) > 0 || len(t.Spec.EphemeralContainers) > 0 {
|
if len(t.Spec.Containers) > 1 || len(t.Spec.InitContainers) > 0 || len(t.Spec.EphemeralContainers) > 0 {
|
||||||
|
if !allPods {
|
||||||
fmt.Fprintf(os.Stderr, "Defaulted container %q out of: %s\n", currOpts.Container, podcmd.AllContainerNames(t))
|
fmt.Fprintf(os.Stderr, "Defaulted container %q out of: %s\n", currOpts.Container, podcmd.AllContainerNames(t))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
container, fieldPath := podcmd.FindContainerByName(t, currOpts.Container)
|
container, fieldPath := podcmd.FindContainerByName(t, currOpts.Container)
|
||||||
if container == nil {
|
if container == nil {
|
||||||
@ -117,7 +132,7 @@ func logsForObjectWithClient(clientset corev1client.CoreV1Interface, object, opt
|
|||||||
for _, c := range t.Spec.InitContainers {
|
for _, c := range t.Spec.InitContainers {
|
||||||
currOpts := opts.DeepCopy()
|
currOpts := opts.DeepCopy()
|
||||||
currOpts.Container = c.Name
|
currOpts.Container = c.Name
|
||||||
currRet, err := logsForObjectWithClient(clientset, t, currOpts, timeout, false)
|
currRet, err := logsForObjectWithClient(clientset, t, currOpts, timeout, false, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -128,7 +143,7 @@ func logsForObjectWithClient(clientset corev1client.CoreV1Interface, object, opt
|
|||||||
for _, c := range t.Spec.Containers {
|
for _, c := range t.Spec.Containers {
|
||||||
currOpts := opts.DeepCopy()
|
currOpts := opts.DeepCopy()
|
||||||
currOpts.Container = c.Name
|
currOpts.Container = c.Name
|
||||||
currRet, err := logsForObjectWithClient(clientset, t, currOpts, timeout, false)
|
currRet, err := logsForObjectWithClient(clientset, t, currOpts, timeout, false, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -139,7 +154,7 @@ func logsForObjectWithClient(clientset corev1client.CoreV1Interface, object, opt
|
|||||||
for _, c := range t.Spec.EphemeralContainers {
|
for _, c := range t.Spec.EphemeralContainers {
|
||||||
currOpts := opts.DeepCopy()
|
currOpts := opts.DeepCopy()
|
||||||
currOpts.Container = c.Name
|
currOpts.Container = c.Name
|
||||||
currRet, err := logsForObjectWithClient(clientset, t, currOpts, timeout, false)
|
currRet, err := logsForObjectWithClient(clientset, t, currOpts, timeout, false, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -161,9 +176,18 @@ func logsForObjectWithClient(clientset corev1client.CoreV1Interface, object, opt
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
var targetObj runtime.Object = pod
|
||||||
|
|
||||||
if numPods > 1 {
|
if numPods > 1 {
|
||||||
|
if allPods {
|
||||||
|
targetObj, err = GetPodList(clientset, namespace, selector.String(), timeout, sortBy)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
fmt.Fprintf(os.Stderr, "Found %v pods, using pod/%v\n", numPods, pod.Name)
|
fmt.Fprintf(os.Stderr, "Found %v pods, using pod/%v\n", numPods, pod.Name)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return logsForObjectWithClient(clientset, pod, options, timeout, allContainers)
|
|
||||||
|
return logsForObjectWithClient(clientset, targetObj, options, timeout, allContainers, allPods)
|
||||||
}
|
}
|
||||||
|
@ -46,6 +46,7 @@ func TestLogsForObject(t *testing.T) {
|
|||||||
obj runtime.Object
|
obj runtime.Object
|
||||||
opts *corev1.PodLogOptions
|
opts *corev1.PodLogOptions
|
||||||
allContainers bool
|
allContainers bool
|
||||||
|
allPods bool
|
||||||
clientsetPods []runtime.Object
|
clientsetPods []runtime.Object
|
||||||
actions []testclient.Action
|
actions []testclient.Action
|
||||||
|
|
||||||
@ -73,6 +74,7 @@ func TestLogsForObject(t *testing.T) {
|
|||||||
obj: testPodWithTwoContainersAndTwoInitAndOneEphemeralContainers(),
|
obj: testPodWithTwoContainersAndTwoInitAndOneEphemeralContainers(),
|
||||||
opts: &corev1.PodLogOptions{},
|
opts: &corev1.PodLogOptions{},
|
||||||
allContainers: true,
|
allContainers: true,
|
||||||
|
allPods: false,
|
||||||
actions: []testclient.Action{
|
actions: []testclient.Action{
|
||||||
getLogsAction("test", &corev1.PodLogOptions{Container: "foo-2-and-2-and-1-initc1"}),
|
getLogsAction("test", &corev1.PodLogOptions{Container: "foo-2-and-2-and-1-initc1"}),
|
||||||
getLogsAction("test", &corev1.PodLogOptions{Container: "foo-2-and-2-and-1-initc2"}),
|
getLogsAction("test", &corev1.PodLogOptions{Container: "foo-2-and-2-and-1-initc2"}),
|
||||||
@ -221,6 +223,7 @@ func TestLogsForObject(t *testing.T) {
|
|||||||
},
|
},
|
||||||
opts: &corev1.PodLogOptions{},
|
opts: &corev1.PodLogOptions{},
|
||||||
allContainers: true,
|
allContainers: true,
|
||||||
|
allPods: false,
|
||||||
actions: []testclient.Action{
|
actions: []testclient.Action{
|
||||||
getLogsAction("test", &corev1.PodLogOptions{Container: "foo-2-and-2-initc1"}),
|
getLogsAction("test", &corev1.PodLogOptions{Container: "foo-2-and-2-initc1"}),
|
||||||
getLogsAction("test", &corev1.PodLogOptions{Container: "foo-2-and-2-initc2"}),
|
getLogsAction("test", &corev1.PodLogOptions{Container: "foo-2-and-2-initc2"}),
|
||||||
@ -385,7 +388,7 @@ func TestLogsForObject(t *testing.T) {
|
|||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
fakeClientset := fakeexternal.NewSimpleClientset(test.clientsetPods...)
|
fakeClientset := fakeexternal.NewSimpleClientset(test.clientsetPods...)
|
||||||
responses, err := logsForObjectWithClient(fakeClientset.CoreV1(), test.obj, test.opts, 20*time.Second, test.allContainers)
|
responses, err := logsForObjectWithClient(fakeClientset.CoreV1(), test.obj, test.opts, 20*time.Second, test.allContainers, test.allPods)
|
||||||
if test.expectedErr == "" && err != nil {
|
if test.expectedErr == "" && err != nil {
|
||||||
t.Errorf("%s: unexpected error: %v", test.name, err)
|
t.Errorf("%s: unexpected error: %v", test.name, err)
|
||||||
continue
|
continue
|
||||||
@ -504,6 +507,7 @@ func TestLogsForObjectWithClient(t *testing.T) {
|
|||||||
podLogOptions *corev1.PodLogOptions
|
podLogOptions *corev1.PodLogOptions
|
||||||
expectedFieldPath string
|
expectedFieldPath string
|
||||||
allContainers bool
|
allContainers bool
|
||||||
|
allPods bool
|
||||||
expectedError string
|
expectedError string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@ -552,6 +556,7 @@ func TestLogsForObjectWithClient(t *testing.T) {
|
|||||||
return pod
|
return pod
|
||||||
},
|
},
|
||||||
allContainers: true,
|
allContainers: true,
|
||||||
|
allPods: false,
|
||||||
podLogOptions: &corev1.PodLogOptions{},
|
podLogOptions: &corev1.PodLogOptions{},
|
||||||
expectedFieldPath: `spec.containers{foo-2-c2}`,
|
expectedFieldPath: `spec.containers{foo-2-c2}`,
|
||||||
},
|
},
|
||||||
@ -561,7 +566,7 @@ func TestLogsForObjectWithClient(t *testing.T) {
|
|||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
pod := tc.podFn()
|
pod := tc.podFn()
|
||||||
fakeClientset := fakeexternal.NewSimpleClientset(pod)
|
fakeClientset := fakeexternal.NewSimpleClientset(pod)
|
||||||
responses, err := logsForObjectWithClient(fakeClientset.CoreV1(), pod, tc.podLogOptions, 20*time.Second, tc.allContainers)
|
responses, err := logsForObjectWithClient(fakeClientset.CoreV1(), pod, tc.podLogOptions, 20*time.Second, tc.allContainers, tc.allPods)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if len(tc.expectedError) > 0 {
|
if len(tc.expectedError) > 0 {
|
||||||
if err.Error() == tc.expectedError {
|
if err.Error() == tc.expectedError {
|
||||||
|
@ -20,26 +20,74 @@ package kubectl
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/onsi/ginkgo/v2"
|
||||||
|
"github.com/onsi/gomega"
|
||||||
|
|
||||||
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
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/util/uuid"
|
"k8s.io/apimachinery/pkg/util/uuid"
|
||||||
clientset "k8s.io/client-go/kubernetes"
|
clientset "k8s.io/client-go/kubernetes"
|
||||||
"k8s.io/kubectl/pkg/cmd/util/podcmd"
|
"k8s.io/kubectl/pkg/cmd/util/podcmd"
|
||||||
"k8s.io/kubernetes/test/e2e/framework"
|
"k8s.io/kubernetes/test/e2e/framework"
|
||||||
|
e2edeployment "k8s.io/kubernetes/test/e2e/framework/deployment"
|
||||||
e2ekubectl "k8s.io/kubernetes/test/e2e/framework/kubectl"
|
e2ekubectl "k8s.io/kubernetes/test/e2e/framework/kubectl"
|
||||||
e2epod "k8s.io/kubernetes/test/e2e/framework/pod"
|
e2epod "k8s.io/kubernetes/test/e2e/framework/pod"
|
||||||
e2eoutput "k8s.io/kubernetes/test/e2e/framework/pod/output"
|
e2eoutput "k8s.io/kubernetes/test/e2e/framework/pod/output"
|
||||||
imageutils "k8s.io/kubernetes/test/utils/image"
|
imageutils "k8s.io/kubernetes/test/utils/image"
|
||||||
admissionapi "k8s.io/pod-security-admission/api"
|
admissionapi "k8s.io/pod-security-admission/api"
|
||||||
|
|
||||||
"github.com/onsi/ginkgo/v2"
|
|
||||||
"github.com/onsi/gomega"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func testingDeployment(name, ns string, numberOfPods int32) appsv1.Deployment {
|
||||||
|
return appsv1.Deployment{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
Namespace: ns,
|
||||||
|
Labels: map[string]string{
|
||||||
|
"name": name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Spec: appsv1.DeploymentSpec{
|
||||||
|
Replicas: &numberOfPods,
|
||||||
|
Selector: &metav1.LabelSelector{
|
||||||
|
MatchLabels: map[string]string{
|
||||||
|
"name": name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Template: v1.PodTemplateSpec{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Labels: map[string]string{
|
||||||
|
"name": name,
|
||||||
|
},
|
||||||
|
Annotations: map[string]string{
|
||||||
|
podcmd.DefaultContainerAnnotationName: "container-2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Spec: v1.PodSpec{
|
||||||
|
Containers: []v1.Container{
|
||||||
|
{
|
||||||
|
Name: "container-1",
|
||||||
|
Image: imageutils.GetE2EImage(imageutils.Agnhost),
|
||||||
|
Args: []string{"logs-generator", "--log-lines-total", "10", "--run-duration", "5s"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "container-2",
|
||||||
|
Image: imageutils.GetE2EImage(imageutils.Agnhost),
|
||||||
|
Args: []string{"logs-generator", "--log-lines-total", "20", "--run-duration", "5s"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RestartPolicy: v1.RestartPolicyAlways,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func testingPod(name, value, defaultContainerName string) v1.Pod {
|
func testingPod(name, value, defaultContainerName string) v1.Pod {
|
||||||
return v1.Pod{
|
return v1.Pod{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
@ -93,7 +141,7 @@ var _ = SIGDescribe("Kubectl logs", func() {
|
|||||||
podName := "logs-generator"
|
podName := "logs-generator"
|
||||||
containerName := "logs-generator"
|
containerName := "logs-generator"
|
||||||
ginkgo.BeforeEach(func() {
|
ginkgo.BeforeEach(func() {
|
||||||
ginkgo.By("creating an pod")
|
ginkgo.By("creating a pod")
|
||||||
// Agnhost image generates logs for a total of 100 lines over 20s.
|
// Agnhost image generates logs for a total of 100 lines over 20s.
|
||||||
e2ekubectl.RunKubectlOrDie(ns, "run", podName, "--image="+imageutils.GetE2EImage(imageutils.Agnhost), "--restart=Never", podRunningTimeoutArg, "--", "logs-generator", "--log-lines-total", "100", "--run-duration", "20s")
|
e2ekubectl.RunKubectlOrDie(ns, "run", podName, "--image="+imageutils.GetE2EImage(imageutils.Agnhost), "--restart=Never", podRunningTimeoutArg, "--", "logs-generator", "--log-lines-total", "100", "--run-duration", "20s")
|
||||||
})
|
})
|
||||||
@ -209,4 +257,100 @@ var _ = SIGDescribe("Kubectl logs", func() {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ginkgo.Describe("all pod logs", func() {
|
||||||
|
ginkgo.Describe("the Deployment has 2 replicas and each pod has 2 containers", func() {
|
||||||
|
var deploy *appsv1.Deployment
|
||||||
|
deployName := "deploy-" + string(uuid.NewUUID())
|
||||||
|
numberReplicas := int32(2)
|
||||||
|
ginkgo.BeforeEach(func(ctx context.Context) {
|
||||||
|
deployClient := c.AppsV1().Deployments(ns)
|
||||||
|
ginkgo.By("constructing the Deployment")
|
||||||
|
deployCopy := testingDeployment(deployName, ns, numberReplicas)
|
||||||
|
deploy = &deployCopy
|
||||||
|
ginkgo.By("creating the Deployment")
|
||||||
|
var err error
|
||||||
|
deploy, err = deployClient.Create(ctx, deploy, metav1.CreateOptions{})
|
||||||
|
if err != nil {
|
||||||
|
framework.Failf("Failed to create Deployment: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = e2edeployment.WaitForDeploymentComplete(c, deploy); err != nil {
|
||||||
|
framework.Failf("Failed to wait for Deployment to complete: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
ginkgo.AfterEach(func() {
|
||||||
|
e2ekubectl.RunKubectlOrDie(ns, "delete", "deploy", deployName)
|
||||||
|
})
|
||||||
|
|
||||||
|
ginkgo.It("should get logs from all pods based on default container", func(ctx context.Context) {
|
||||||
|
ginkgo.By("Waiting for Deployment pods to be running.")
|
||||||
|
|
||||||
|
// get the pod names
|
||||||
|
pods, err := e2edeployment.GetPodsForDeployment(ctx, c, deploy)
|
||||||
|
if err != nil {
|
||||||
|
framework.Failf("Failed to get pods for Deployment: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
podOne := pods.Items[0].GetName()
|
||||||
|
podTwo := pods.Items[1].GetName()
|
||||||
|
|
||||||
|
ginkgo.By("expecting logs from both replicas in Deployment")
|
||||||
|
out := e2ekubectl.RunKubectlOrDie(ns, "logs", fmt.Sprintf("deploy/%s", deployName), "--all-pods")
|
||||||
|
framework.Logf("got output %q", out)
|
||||||
|
logLines := strings.Split(out, "\n")
|
||||||
|
logFound := false
|
||||||
|
for _, line := range logLines {
|
||||||
|
var deployPod bool
|
||||||
|
if line != "" {
|
||||||
|
if strings.Contains(line, fmt.Sprintf("[pod/%s/container-2]", podOne)) || strings.Contains(line, fmt.Sprintf("[pod/%s/container-2]", podTwo)) {
|
||||||
|
logFound = true
|
||||||
|
deployPod = true
|
||||||
|
}
|
||||||
|
gomega.Expect(deployPod).To(gomega.BeTrueBecause("each log should be from the default container from each pod in the Deployment"))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
gomega.Expect(logFound).To(gomega.BeTrueBecause("log should be present"))
|
||||||
|
})
|
||||||
|
|
||||||
|
ginkgo.It("should get logs from each pod and each container in Deployment", func(ctx context.Context) {
|
||||||
|
ginkgo.By("Waiting for Deployment pods to be running.")
|
||||||
|
|
||||||
|
pods, err := e2edeployment.GetPodsForDeployment(ctx, c, deploy)
|
||||||
|
if err != nil {
|
||||||
|
framework.Failf("Failed to get pods for Deployment: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
podOne := pods.Items[0].GetName()
|
||||||
|
podTwo := pods.Items[1].GetName()
|
||||||
|
|
||||||
|
ginkgo.By("all containers and all containers")
|
||||||
|
out := e2ekubectl.RunKubectlOrDie(ns, "logs", fmt.Sprintf("deploy/%s", deployName), "--all-pods", "--all-containers")
|
||||||
|
framework.Logf("got output %q", out)
|
||||||
|
logLines := strings.Split(out, "\n")
|
||||||
|
logFound := false
|
||||||
|
for _, line := range logLines {
|
||||||
|
if line != "" {
|
||||||
|
var deployPodContainer bool
|
||||||
|
if strings.Contains(line, fmt.Sprintf("[pod/%s/container-1]", podOne)) || strings.Contains(line, fmt.Sprintf("[pod/%s/container-1]", podTwo)) || strings.Contains(line, fmt.Sprintf("[pod/%s/container-2]", podOne)) || strings.Contains(line, fmt.Sprintf("[pod/%s/container-2]", podTwo)) {
|
||||||
|
logFound = true
|
||||||
|
deployPodContainer = true
|
||||||
|
}
|
||||||
|
gomega.Expect(deployPodContainer).To(gomega.BeTrueBecause("each log should be from all containers from all pods in the Deployment"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gomega.Expect(logFound).To(gomega.BeTrueBecause("log should be present"))
|
||||||
|
gomega.Expect(strings.Contains(out, fmt.Sprintf("[pod/%s/container-1]", podOne))).To(gomega.BeTrueBecause("pod 1 container 1 log should be present"))
|
||||||
|
gomega.Expect(strings.Contains(out, fmt.Sprintf("[pod/%s/container-2]", podOne))).To(gomega.BeTrueBecause("pod 1 container 2 log should be present"))
|
||||||
|
gomega.Expect(strings.Contains(out, fmt.Sprintf("[pod/%s/container-1]", podTwo))).To(gomega.BeTrueBecause("pod 2 container 1 log should be present"))
|
||||||
|
gomega.Expect(strings.Contains(out, fmt.Sprintf("[pod/%s/container-2]", podTwo))).To(gomega.BeTrueBecause("pod 2 container 2 log should be present"))
|
||||||
|
gomega.Expect(out).NotTo(gomega.BeEmpty())
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user