mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-01 07:47:56 +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
|
||||
ResourceArg string
|
||||
AllContainers bool
|
||||
AllPods bool
|
||||
Options runtime.Object
|
||||
Resources []string
|
||||
|
||||
@ -122,10 +123,11 @@ type LogsOptions struct {
|
||||
MaxFollowConcurrency int
|
||||
Prefix bool
|
||||
|
||||
Object runtime.Object
|
||||
GetPodTimeout time.Duration
|
||||
RESTClientGetter genericclioptions.RESTClientGetter
|
||||
LogsForObject polymorphichelpers.LogsForObjectFunc
|
||||
Object runtime.Object
|
||||
GetPodTimeout time.Duration
|
||||
RESTClientGetter genericclioptions.RESTClientGetter
|
||||
LogsForObject polymorphichelpers.LogsForObjectFunc
|
||||
AllPodLogsForObject polymorphichelpers.AllPodLogsForObjectFunc
|
||||
|
||||
genericiooptions.IOStreams
|
||||
|
||||
@ -134,10 +136,9 @@ type LogsOptions struct {
|
||||
containerNameFromRefSpecRegexp *regexp.Regexp
|
||||
}
|
||||
|
||||
func NewLogsOptions(streams genericiooptions.IOStreams, allContainers bool) *LogsOptions {
|
||||
func NewLogsOptions(streams genericiooptions.IOStreams) *LogsOptions {
|
||||
return &LogsOptions{
|
||||
IOStreams: streams,
|
||||
AllContainers: allContainers,
|
||||
Tail: -1,
|
||||
MaxFollowConcurrency: 5,
|
||||
|
||||
@ -147,7 +148,7 @@ func NewLogsOptions(streams genericiooptions.IOStreams, allContainers bool) *Log
|
||||
|
||||
// NewCmdLogs creates a new pod logs command
|
||||
func NewCmdLogs(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
|
||||
o := NewLogsOptions(streams, false)
|
||||
o := NewLogsOptions(streams)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: logsUsageStr,
|
||||
@ -167,6 +168,7 @@ func NewCmdLogs(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Co
|
||||
}
|
||||
|
||||
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().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")
|
||||
@ -243,6 +245,11 @@ func (o *LogsOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []str
|
||||
default:
|
||||
return cmdutil.UsageErrorf(cmd, "%s", logsUsageErrStr)
|
||||
}
|
||||
|
||||
if o.AllPods {
|
||||
o.Prefix = true
|
||||
}
|
||||
|
||||
var err error
|
||||
o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace()
|
||||
if err != nil {
|
||||
@ -263,6 +270,7 @@ func (o *LogsOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []str
|
||||
|
||||
o.RESTClientGetter = f
|
||||
o.LogsForObject = polymorphichelpers.LogsForObjectFn
|
||||
o.AllPodLogsForObject = polymorphichelpers.AllPodLogsForObjectFn
|
||||
|
||||
if o.Object == nil {
|
||||
builder := f.NewBuilder().
|
||||
@ -328,7 +336,13 @@ func (o LogsOptions) Validate() error {
|
||||
|
||||
// RunLogs retrieves a pod log
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ import (
|
||||
"testing/iotest"
|
||||
"time"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
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.ConsumeRequestFn = mock.mockConsumeRequest
|
||||
|
||||
@ -83,7 +84,7 @@ func TestLog(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
o := NewLogsOptions(streams, false)
|
||||
o := NewLogsOptions(streams)
|
||||
o.LogsForObject = mock.mockLogsForObject
|
||||
o.ConsumeRequestFn = mock.mockConsumeRequest
|
||||
o.Prefix = true
|
||||
@ -92,6 +93,32 @@ func TestLog(t *testing.T) {
|
||||
},
|
||||
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",
|
||||
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.ConsumeRequestFn = mock.mockConsumeRequest
|
||||
o.Prefix = true
|
||||
@ -127,7 +154,7 @@ func TestLog(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
o := NewLogsOptions(streams, false)
|
||||
o := NewLogsOptions(streams)
|
||||
o.LogsForObject = mock.mockLogsForObject
|
||||
o.ConsumeRequestFn = mock.mockConsumeRequest
|
||||
o.Prefix = true
|
||||
@ -159,7 +186,7 @@ func TestLog(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
o := NewLogsOptions(streams, false)
|
||||
o := NewLogsOptions(streams)
|
||||
o.LogsForObject = mock.mockLogsForObject
|
||||
o.ConsumeRequestFn = mock.mockConsumeRequest
|
||||
return o
|
||||
@ -196,7 +223,7 @@ func TestLog(t *testing.T) {
|
||||
}
|
||||
wg.Add(3)
|
||||
|
||||
o := NewLogsOptions(streams, false)
|
||||
o := NewLogsOptions(streams)
|
||||
o.LogsForObject = mock.mockLogsForObject
|
||||
o.ConsumeRequestFn = mock.mockConsumeRequest
|
||||
o.Follow = true
|
||||
@ -234,7 +261,7 @@ func TestLog(t *testing.T) {
|
||||
}
|
||||
wg.Add(3)
|
||||
|
||||
o := NewLogsOptions(streams, false)
|
||||
o := NewLogsOptions(streams)
|
||||
o.LogsForObject = mock.mockLogsForObject
|
||||
o.ConsumeRequestFn = mock.mockConsumeRequest
|
||||
o.MaxFollowConcurrency = 2
|
||||
@ -246,7 +273,7 @@ func TestLog(t *testing.T) {
|
||||
{
|
||||
name: "fail if LogsForObject fails",
|
||||
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) {
|
||||
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.ConsumeRequestFn = func(req restclient.ResponseWrapper, out io.Writer) error {
|
||||
return errors.New("Error from the ConsumeRequestFn")
|
||||
@ -307,7 +334,7 @@ func TestLog(t *testing.T) {
|
||||
}
|
||||
wg.Add(3)
|
||||
|
||||
o := NewLogsOptions(streams, false)
|
||||
o := NewLogsOptions(streams)
|
||||
o.LogsForObject = mock.mockLogsForObject
|
||||
o.ConsumeRequestFn = mock.mockConsumeRequest
|
||||
o.Follow = true
|
||||
@ -346,7 +373,7 @@ func TestLog(t *testing.T) {
|
||||
}
|
||||
wg.Add(3)
|
||||
|
||||
o := NewLogsOptions(streams, false)
|
||||
o := NewLogsOptions(streams)
|
||||
o.LogsForObject = mock.mockLogsForObject
|
||||
o.ConsumeRequestFn = func(req restclient.ResponseWrapper, out io.Writer) error {
|
||||
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.ConsumeRequestFn = func(req restclient.ResponseWrapper, out io.Writer) error {
|
||||
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.ConsumeRequestFn = mock.mockConsumeRequest
|
||||
o.IgnoreLogErrors = true
|
||||
@ -432,7 +459,7 @@ func TestLog(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
o := NewLogsOptions(streams, false)
|
||||
o := NewLogsOptions(streams)
|
||||
o.LogsForObject = mock.mockLogsForObject
|
||||
o.ConsumeRequestFn = mock.mockConsumeRequest
|
||||
return o
|
||||
@ -462,7 +489,7 @@ func TestLog(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
o := NewLogsOptions(streams, false)
|
||||
o := NewLogsOptions(streams)
|
||||
o.LogsForObject = mock.mockLogsForObject
|
||||
o.ConsumeRequestFn = mock.mockConsumeRequest
|
||||
o.IgnoreLogErrors = true
|
||||
@ -493,7 +520,7 @@ func TestLog(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
o := NewLogsOptions(streams, false)
|
||||
o := NewLogsOptions(streams)
|
||||
o.LogsForObject = mock.mockLogsForObject
|
||||
o.ConsumeRequestFn = mock.mockConsumeRequest
|
||||
o.Follow = true
|
||||
@ -564,7 +591,7 @@ func TestValidateLogOptions(t *testing.T) {
|
||||
{
|
||||
name: "since & since-time",
|
||||
opts: func(streams genericiooptions.IOStreams) *LogsOptions {
|
||||
o := NewLogsOptions(streams, false)
|
||||
o := NewLogsOptions(streams)
|
||||
o.SinceSeconds = time.Hour
|
||||
o.SinceTime = "2006-01-02T15:04:05Z"
|
||||
|
||||
@ -582,7 +609,7 @@ func TestValidateLogOptions(t *testing.T) {
|
||||
{
|
||||
name: "negative since-time",
|
||||
opts: func(streams genericiooptions.IOStreams) *LogsOptions {
|
||||
o := NewLogsOptions(streams, false)
|
||||
o := NewLogsOptions(streams)
|
||||
o.SinceSeconds = -1 * time.Second
|
||||
|
||||
var err error
|
||||
@ -599,7 +626,7 @@ func TestValidateLogOptions(t *testing.T) {
|
||||
{
|
||||
name: "negative limit-bytes",
|
||||
opts: func(streams genericiooptions.IOStreams) *LogsOptions {
|
||||
o := NewLogsOptions(streams, false)
|
||||
o := NewLogsOptions(streams)
|
||||
o.LimitBytes = -100
|
||||
|
||||
var err error
|
||||
@ -616,7 +643,7 @@ func TestValidateLogOptions(t *testing.T) {
|
||||
{
|
||||
name: "negative tail",
|
||||
opts: func(streams genericiooptions.IOStreams) *LogsOptions {
|
||||
o := NewLogsOptions(streams, false)
|
||||
o := NewLogsOptions(streams)
|
||||
o.Tail = -100
|
||||
|
||||
var err error
|
||||
@ -633,7 +660,8 @@ func TestValidateLogOptions(t *testing.T) {
|
||||
{
|
||||
name: "container name combined with --all-containers",
|
||||
opts: func(streams genericiooptions.IOStreams) *LogsOptions {
|
||||
o := NewLogsOptions(streams, true)
|
||||
o := NewLogsOptions(streams)
|
||||
o.AllContainers = true
|
||||
o.Container = "my-container"
|
||||
|
||||
var err error
|
||||
@ -650,7 +678,7 @@ func TestValidateLogOptions(t *testing.T) {
|
||||
{
|
||||
name: "container name combined with second argument",
|
||||
opts: func(streams genericiooptions.IOStreams) *LogsOptions {
|
||||
o := NewLogsOptions(streams, false)
|
||||
o := NewLogsOptions(streams)
|
||||
o.Container = "my-container"
|
||||
o.ContainerNameSpecified = true
|
||||
|
||||
@ -697,7 +725,7 @@ func TestLogComplete(t *testing.T) {
|
||||
name: "One args case",
|
||||
args: []string{"foo"},
|
||||
opts: func(streams genericiooptions.IOStreams) *LogsOptions {
|
||||
o := NewLogsOptions(streams, false)
|
||||
o := NewLogsOptions(streams)
|
||||
o.Selector = "foo"
|
||||
return o
|
||||
},
|
||||
@ -816,7 +844,7 @@ func TestNoResourceFoundMessage(t *testing.T) {
|
||||
|
||||
streams, _, buf, errbuf := genericiooptions.NewTestIOStreams()
|
||||
cmd := NewCmdLogs(tf, streams)
|
||||
o := NewLogsOptions(streams, false)
|
||||
o := NewLogsOptions(streams)
|
||||
o.Selector = "foo"
|
||||
err := o.Complete(tf, cmd, []string{})
|
||||
|
||||
@ -864,7 +892,7 @@ func TestNoPodInNamespaceFoundMessage(t *testing.T) {
|
||||
|
||||
streams, _, _, _ := genericiooptions.NewTestIOStreams()
|
||||
cmd := NewCmdLogs(tf, streams)
|
||||
o := NewLogsOptions(streams, false)
|
||||
o := NewLogsOptions(streams)
|
||||
err := o.Complete(tf, cmd, []string{podName})
|
||||
|
||||
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) {
|
||||
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:
|
||||
_, ok := options.(*corev1.PodLogOptions)
|
||||
if !ok {
|
||||
|
@ -36,15 +36,15 @@ import (
|
||||
watchtools "k8s.io/client-go/tools/watch"
|
||||
)
|
||||
|
||||
// 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) {
|
||||
// GetPodList returns a PodList matching the namespace and label selector
|
||||
func GetPodList(client coreclient.PodsGetter, namespace string, selector string, timeout time.Duration, sortBy func([]*corev1.Pod) sort.Interface) (*corev1.PodList, error) {
|
||||
options := metav1.ListOptions{LabelSelector: selector}
|
||||
|
||||
podList, err := client.Pods(namespace).List(context.TODO(), options)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pods := []*corev1.Pod{}
|
||||
for i := range podList.Items {
|
||||
pod := podList.Items[i]
|
||||
@ -52,14 +52,17 @@ func GetFirstPod(client coreclient.PodsGetter, namespace string, selector string
|
||||
}
|
||||
if len(pods) > 0 {
|
||||
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
|
||||
options.ResourceVersion = podList.ResourceVersion
|
||||
w, err := client.Pods(namespace).Watch(context.TODO(), options)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
return nil, err
|
||||
}
|
||||
defer w.Stop()
|
||||
|
||||
@ -70,14 +73,30 @@ func GetFirstPod(client coreclient.PodsGetter, namespace string, selector string
|
||||
ctx, cancel := watchtools.ContextWithOptionalTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
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 {
|
||||
return nil, 0, err
|
||||
}
|
||||
pod, ok := event.Object.(*corev1.Pod)
|
||||
if !ok {
|
||||
return nil, 0, fmt.Errorf("%#v is not a pod event", event)
|
||||
}
|
||||
return pod, 1, nil
|
||||
|
||||
return &podList.Items[0], len(podList.Items), nil
|
||||
|
||||
}
|
||||
|
||||
// SelectorsForObject returns the pod label selector for a given object
|
||||
|
@ -32,6 +32,98 @@ import (
|
||||
"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) {
|
||||
labelSet := map[string]string{"test": "selector"}
|
||||
tests := []struct {
|
||||
|
@ -27,6 +27,12 @@ import (
|
||||
"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
|
||||
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"
|
||||
)
|
||||
|
||||
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) {
|
||||
clientConfig, err := restClientGetter.ToRESTConfig()
|
||||
if err != nil {
|
||||
@ -44,11 +57,11 @@ func logsForObject(restClientGetter genericclioptions.RESTClientGetter, object,
|
||||
if err != nil {
|
||||
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
|
||||
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)
|
||||
if !ok {
|
||||
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:
|
||||
ret := make(map[corev1.ObjectReference]rest.ResponseWrapper)
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
@ -95,7 +108,9 @@ func logsForObjectWithClient(clientset corev1client.CoreV1Interface, object, opt
|
||||
// Default to the first container name(aligning behavior with `kubectl exec').
|
||||
currOpts.Container = t.Spec.Containers[0].Name
|
||||
if len(t.Spec.Containers) > 1 || len(t.Spec.InitContainers) > 0 || len(t.Spec.EphemeralContainers) > 0 {
|
||||
fmt.Fprintf(os.Stderr, "Defaulted container %q out of: %s\n", currOpts.Container, podcmd.AllContainerNames(t))
|
||||
if !allPods {
|
||||
fmt.Fprintf(os.Stderr, "Defaulted container %q out of: %s\n", currOpts.Container, podcmd.AllContainerNames(t))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,7 +132,7 @@ func logsForObjectWithClient(clientset corev1client.CoreV1Interface, object, opt
|
||||
for _, c := range t.Spec.InitContainers {
|
||||
currOpts := opts.DeepCopy()
|
||||
currOpts.Container = c.Name
|
||||
currRet, err := logsForObjectWithClient(clientset, t, currOpts, timeout, false)
|
||||
currRet, err := logsForObjectWithClient(clientset, t, currOpts, timeout, false, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -128,7 +143,7 @@ func logsForObjectWithClient(clientset corev1client.CoreV1Interface, object, opt
|
||||
for _, c := range t.Spec.Containers {
|
||||
currOpts := opts.DeepCopy()
|
||||
currOpts.Container = c.Name
|
||||
currRet, err := logsForObjectWithClient(clientset, t, currOpts, timeout, false)
|
||||
currRet, err := logsForObjectWithClient(clientset, t, currOpts, timeout, false, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -139,7 +154,7 @@ func logsForObjectWithClient(clientset corev1client.CoreV1Interface, object, opt
|
||||
for _, c := range t.Spec.EphemeralContainers {
|
||||
currOpts := opts.DeepCopy()
|
||||
currOpts.Container = c.Name
|
||||
currRet, err := logsForObjectWithClient(clientset, t, currOpts, timeout, false)
|
||||
currRet, err := logsForObjectWithClient(clientset, t, currOpts, timeout, false, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -161,9 +176,18 @@ func logsForObjectWithClient(clientset corev1client.CoreV1Interface, object, opt
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var targetObj runtime.Object = pod
|
||||
|
||||
if numPods > 1 {
|
||||
fmt.Fprintf(os.Stderr, "Found %v pods, using pod/%v\n", numPods, pod.Name)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
opts *corev1.PodLogOptions
|
||||
allContainers bool
|
||||
allPods bool
|
||||
clientsetPods []runtime.Object
|
||||
actions []testclient.Action
|
||||
|
||||
@ -73,6 +74,7 @@ func TestLogsForObject(t *testing.T) {
|
||||
obj: testPodWithTwoContainersAndTwoInitAndOneEphemeralContainers(),
|
||||
opts: &corev1.PodLogOptions{},
|
||||
allContainers: true,
|
||||
allPods: false,
|
||||
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-initc2"}),
|
||||
@ -221,6 +223,7 @@ func TestLogsForObject(t *testing.T) {
|
||||
},
|
||||
opts: &corev1.PodLogOptions{},
|
||||
allContainers: true,
|
||||
allPods: false,
|
||||
actions: []testclient.Action{
|
||||
getLogsAction("test", &corev1.PodLogOptions{Container: "foo-2-and-2-initc1"}),
|
||||
getLogsAction("test", &corev1.PodLogOptions{Container: "foo-2-and-2-initc2"}),
|
||||
@ -385,7 +388,7 @@ func TestLogsForObject(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
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 {
|
||||
t.Errorf("%s: unexpected error: %v", test.name, err)
|
||||
continue
|
||||
@ -504,6 +507,7 @@ func TestLogsForObjectWithClient(t *testing.T) {
|
||||
podLogOptions *corev1.PodLogOptions
|
||||
expectedFieldPath string
|
||||
allContainers bool
|
||||
allPods bool
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
@ -552,6 +556,7 @@ func TestLogsForObjectWithClient(t *testing.T) {
|
||||
return pod
|
||||
},
|
||||
allContainers: true,
|
||||
allPods: false,
|
||||
podLogOptions: &corev1.PodLogOptions{},
|
||||
expectedFieldPath: `spec.containers{foo-2-c2}`,
|
||||
},
|
||||
@ -561,7 +566,7 @@ func TestLogsForObjectWithClient(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
pod := tc.podFn()
|
||||
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 len(tc.expectedError) > 0 {
|
||||
if err.Error() == tc.expectedError {
|
||||
|
@ -20,26 +20,74 @@ package kubectl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/onsi/ginkgo/v2"
|
||||
"github.com/onsi/gomega"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/uuid"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
"k8s.io/kubectl/pkg/cmd/util/podcmd"
|
||||
"k8s.io/kubernetes/test/e2e/framework"
|
||||
e2edeployment "k8s.io/kubernetes/test/e2e/framework/deployment"
|
||||
e2ekubectl "k8s.io/kubernetes/test/e2e/framework/kubectl"
|
||||
e2epod "k8s.io/kubernetes/test/e2e/framework/pod"
|
||||
e2eoutput "k8s.io/kubernetes/test/e2e/framework/pod/output"
|
||||
imageutils "k8s.io/kubernetes/test/utils/image"
|
||||
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 {
|
||||
return v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
@ -93,7 +141,7 @@ var _ = SIGDescribe("Kubectl logs", func() {
|
||||
podName := "logs-generator"
|
||||
containerName := "logs-generator"
|
||||
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.
|
||||
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