Skip to content

Commit 9660e5c

Browse files
authored
Merge pull request kubernetes#127360 from knight42/feat/split-stdout-stderr-server-side
API: add a new `Stream` field to `PodLogOptions`
2 parents 9c571ab + a137b08 commit 9660e5c

37 files changed

+1729
-959
lines changed

api/openapi-spec/swagger.json

Lines changed: 13 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/openapi-spec/v3/api__v1_openapi.json

Lines changed: 10 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/apis/core/types.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5572,6 +5572,15 @@ type Preconditions struct {
55725572
UID *types.UID
55735573
}
55745574

5575+
const (
5576+
// LogStreamStdout is the stream type for stdout.
5577+
LogStreamStdout = "Stdout"
5578+
// LogStreamStderr is the stream type for stderr.
5579+
LogStreamStderr = "Stderr"
5580+
// LogStreamAll represents the combined stdout and stderr.
5581+
LogStreamAll = "All"
5582+
)
5583+
55755584
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
55765585

55775586
// PodLogOptions is the query options for a Pod's logs REST call
@@ -5598,7 +5607,8 @@ type PodLogOptions struct {
55985607
// of log output.
55995608
Timestamps bool
56005609
// If set, the number of lines from the end of the logs to show. If not specified,
5601-
// logs are shown from the creation of the container or sinceSeconds or sinceTime
5610+
// logs are shown from the creation of the container or sinceSeconds or sinceTime.
5611+
// Note that when "TailLines" is specified, "Stream" can only be set to nil or "All".
56025612
TailLines *int64
56035613
// If set, the number of bytes to read from the server before terminating the
56045614
// log output. This may not display a complete final line of logging, and may return
@@ -5613,6 +5623,14 @@ type PodLogOptions struct {
56135623
// the actual log data coming from the real kubelet).
56145624
// +optional
56155625
InsecureSkipTLSVerifyBackend bool
5626+
5627+
// Specify which container log stream to return to the client.
5628+
// Acceptable values are "All", "Stdout" and "Stderr". If not specified, "All" is used, and both stdout and stderr
5629+
// are returned interleaved.
5630+
// Note that when "TailLines" is specified, "Stream" can only be set to nil or "All".
5631+
// +featureGate=PodLogsQuerySplitStreams
5632+
// +optional
5633+
Stream *string
56165634
}
56175635

56185636
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

pkg/apis/core/v1/conversion.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,15 @@ import (
2020
"fmt"
2121
"reflect"
2222

23-
v1 "k8s.io/api/core/v1"
23+
"k8s.io/utils/ptr"
2424

25+
v1 "k8s.io/api/core/v1"
2526
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2627
"k8s.io/apimachinery/pkg/conversion"
2728
"k8s.io/apimachinery/pkg/runtime"
2829
"k8s.io/apimachinery/pkg/util/validation/field"
2930
"k8s.io/kubernetes/pkg/apis/apps"
3031
"k8s.io/kubernetes/pkg/apis/core"
31-
utilpointer "k8s.io/utils/pointer"
3232
)
3333

3434
func addConversionFuncs(scheme *runtime.Scheme) error {
@@ -380,7 +380,7 @@ func Convert_v1_Pod_To_core_Pod(in *v1.Pod, out *core.Pod, s conversion.Scope) e
380380
// Forcing the value of TerminationGracePeriodSeconds to 1 if it is negative.
381381
// Just for Pod, not for PodSpec, because we don't want to change the behavior of the PodTemplate.
382382
if in.Spec.TerminationGracePeriodSeconds != nil && *in.Spec.TerminationGracePeriodSeconds < 0 {
383-
out.Spec.TerminationGracePeriodSeconds = utilpointer.Int64(1)
383+
out.Spec.TerminationGracePeriodSeconds = ptr.To[int64](1)
384384
}
385385
return nil
386386
}
@@ -397,7 +397,7 @@ func Convert_core_Pod_To_v1_Pod(in *core.Pod, out *v1.Pod, s conversion.Scope) e
397397
// Forcing the value of TerminationGracePeriodSeconds to 1 if it is negative.
398398
// Just for Pod, not for PodSpec, because we don't want to change the behavior of the PodTemplate.
399399
if in.Spec.TerminationGracePeriodSeconds != nil && *in.Spec.TerminationGracePeriodSeconds < 0 {
400-
out.Spec.TerminationGracePeriodSeconds = utilpointer.Int64(1)
400+
out.Spec.TerminationGracePeriodSeconds = ptr.To[int64](1)
401401
}
402402
return nil
403403
}
@@ -554,3 +554,13 @@ func Convert_core_PersistentVolumeSpec_To_v1_PersistentVolumeSpec(in *core.Persi
554554
func Convert_v1_PersistentVolumeSpec_To_core_PersistentVolumeSpec(in *v1.PersistentVolumeSpec, out *core.PersistentVolumeSpec, s conversion.Scope) error {
555555
return autoConvert_v1_PersistentVolumeSpec_To_core_PersistentVolumeSpec(in, out, s)
556556
}
557+
558+
// Convert_Slice_string_To_Pointer_string is needed because decoding URL parameters requires manual assistance.
559+
func Convert_Slice_string_To_Pointer_string(in *[]string, out **string, s conversion.Scope) error {
560+
if len(*in) == 0 {
561+
return nil
562+
}
563+
temp := (*in)[0]
564+
*out = &temp
565+
return nil
566+
}

pkg/apis/core/v1/conversion_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ func TestPodLogOptions(t *testing.T) {
5252
sinceTime := metav1.NewTime(time.Date(2000, 1, 1, 12, 34, 56, 0, time.UTC).Local())
5353
tailLines := int64(2)
5454
limitBytes := int64(3)
55+
v1StreamStderr := v1.LogStreamStderr
56+
coreStreamStderr := core.LogStreamStderr
5557

5658
versionedLogOptions := &v1.PodLogOptions{
5759
Container: "mycontainer",
@@ -62,6 +64,7 @@ func TestPodLogOptions(t *testing.T) {
6264
Timestamps: true,
6365
TailLines: &tailLines,
6466
LimitBytes: &limitBytes,
67+
Stream: &v1StreamStderr,
6568
}
6669
unversionedLogOptions := &core.PodLogOptions{
6770
Container: "mycontainer",
@@ -72,6 +75,7 @@ func TestPodLogOptions(t *testing.T) {
7275
Timestamps: true,
7376
TailLines: &tailLines,
7477
LimitBytes: &limitBytes,
78+
Stream: &coreStreamStderr,
7579
}
7680
expectedParameters := url.Values{
7781
"container": {"mycontainer"},
@@ -82,6 +86,7 @@ func TestPodLogOptions(t *testing.T) {
8286
"timestamps": {"true"},
8387
"tailLines": {"2"},
8488
"limitBytes": {"3"},
89+
"stream": {"Stderr"},
8590
}
8691

8792
codec := runtime.NewParameterCodec(legacyscheme.Scheme)

pkg/apis/core/v1/defaults.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,15 @@ package v1
1919
import (
2020
"time"
2121

22+
"k8s.io/utils/ptr"
23+
2224
v1 "k8s.io/api/core/v1"
2325
"k8s.io/apimachinery/pkg/runtime"
2426
"k8s.io/apimachinery/pkg/util/intstr"
2527
utilfeature "k8s.io/apiserver/pkg/util/feature"
2628
"k8s.io/kubernetes/pkg/api/v1/service"
2729
"k8s.io/kubernetes/pkg/features"
2830
"k8s.io/kubernetes/pkg/util/parsers"
29-
"k8s.io/utils/pointer"
3031
)
3132

3233
func addDefaultingFuncs(scheme *runtime.Scheme) error {
@@ -64,7 +65,7 @@ func SetDefaults_ReplicationController(obj *v1.ReplicationController) {
6465
}
6566
}
6667
func SetDefaults_Volume(obj *v1.Volume) {
67-
if pointer.AllPtrFieldsNil(&obj.VolumeSource) {
68+
if ptr.AllPtrFieldsNil(&obj.VolumeSource) {
6869
obj.VolumeSource = v1.VolumeSource{
6970
EmptyDir: &v1.EmptyDirVolumeSource{},
7071
}
@@ -147,7 +148,7 @@ func SetDefaults_Service(obj *v1.Service) {
147148

148149
if obj.Spec.Type == v1.ServiceTypeLoadBalancer {
149150
if obj.Spec.AllocateLoadBalancerNodePorts == nil {
150-
obj.Spec.AllocateLoadBalancerNodePorts = pointer.Bool(true)
151+
obj.Spec.AllocateLoadBalancerNodePorts = ptr.To(true)
151152
}
152153
}
153154

@@ -429,3 +430,11 @@ func SetDefaults_HostPathVolumeSource(obj *v1.HostPathVolumeSource) {
429430
obj.Type = &typeVol
430431
}
431432
}
433+
434+
func SetDefaults_PodLogOptions(obj *v1.PodLogOptions) {
435+
if utilfeature.DefaultFeatureGate.Enabled(features.PodLogsQuerySplitStreams) {
436+
if obj.Stream == nil {
437+
obj.Stream = ptr.To(v1.LogStreamAll)
438+
}
439+
}
440+
}

pkg/apis/core/v1/defaults_test.go

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import (
2424
"testing"
2525

2626
"github.com/google/go-cmp/cmp"
27+
"k8s.io/utils/ptr"
28+
2729
v1 "k8s.io/api/core/v1"
2830
"k8s.io/apimachinery/pkg/api/resource"
2931
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -35,7 +37,6 @@ import (
3537
"k8s.io/kubernetes/pkg/api/legacyscheme"
3638
corev1 "k8s.io/kubernetes/pkg/apis/core/v1"
3739
"k8s.io/kubernetes/pkg/features"
38-
utilpointer "k8s.io/utils/pointer"
3940

4041
// ensure types are installed
4142
_ "k8s.io/kubernetes/pkg/apis/core/install"
@@ -690,7 +691,7 @@ func TestSetDefaultReplicationControllerReplicas(t *testing.T) {
690691
{
691692
rc: v1.ReplicationController{
692693
Spec: v1.ReplicationControllerSpec{
693-
Replicas: utilpointer.Int32(0),
694+
Replicas: ptr.To[int32](0),
694695
Template: &v1.PodTemplateSpec{
695696
ObjectMeta: metav1.ObjectMeta{
696697
Labels: map[string]string{
@@ -705,7 +706,7 @@ func TestSetDefaultReplicationControllerReplicas(t *testing.T) {
705706
{
706707
rc: v1.ReplicationController{
707708
Spec: v1.ReplicationControllerSpec{
708-
Replicas: utilpointer.Int32(3),
709+
Replicas: ptr.To[int32](3),
709710
Template: &v1.PodTemplateSpec{
710711
ObjectMeta: metav1.ObjectMeta{
711712
Labels: map[string]string{
@@ -1930,7 +1931,7 @@ func TestDefaultRequestIsNotSetForReplicationController(t *testing.T) {
19301931
}
19311932
rc := &v1.ReplicationController{
19321933
Spec: v1.ReplicationControllerSpec{
1933-
Replicas: utilpointer.Int32(3),
1934+
Replicas: ptr.To[int32](3),
19341935
Template: &v1.PodTemplateSpec{
19351936
ObjectMeta: metav1.ObjectMeta{
19361937
Labels: map[string]string{
@@ -2349,3 +2350,26 @@ func TestSetDefaults_Volume(t *testing.T) {
23492350
})
23502351
}
23512352
}
2353+
2354+
func TestSetDefaults_PodLogOptions(t *testing.T) {
2355+
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.PodLogsQuerySplitStreams, true)
2356+
for desc, tc := range map[string]struct {
2357+
given, expected *v1.PodLogOptions
2358+
}{
2359+
"defaults to All": {
2360+
given: &v1.PodLogOptions{},
2361+
expected: &v1.PodLogOptions{Stream: ptr.To(v1.LogStreamAll)},
2362+
},
2363+
"the specified stream should not be overridden": {
2364+
given: &v1.PodLogOptions{Stream: ptr.To(v1.LogStreamStdout)},
2365+
expected: &v1.PodLogOptions{Stream: ptr.To(v1.LogStreamStdout)},
2366+
},
2367+
} {
2368+
t.Run(desc, func(t *testing.T) {
2369+
corev1.SetDefaults_PodLogOptions(tc.given)
2370+
if !cmp.Equal(tc.given, tc.expected) {
2371+
t.Errorf("expected volume %+v, but got %+v", tc.expected, tc.given)
2372+
}
2373+
})
2374+
}
2375+
}

pkg/apis/core/v1/validation/validation.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"k8s.io/apimachinery/pkg/api/resource"
2525
"k8s.io/apimachinery/pkg/util/sets"
2626
"k8s.io/apimachinery/pkg/util/validation/field"
27+
2728
"k8s.io/kubernetes/pkg/apis/core"
2829
"k8s.io/kubernetes/pkg/apis/core/helper"
2930
v1helper "k8s.io/kubernetes/pkg/apis/core/v1/helper"
@@ -124,6 +125,12 @@ func validateResourceName(value core.ResourceName, fldPath *field.Path) field.Er
124125
return allErrs
125126
}
126127

128+
var validLogStreams = sets.New[string](
129+
v1.LogStreamStdout,
130+
v1.LogStreamStderr,
131+
v1.LogStreamAll,
132+
)
133+
127134
// ValidatePodLogOptions checks if options that are set are at the correct
128135
// value. Any incorrect value will be returned to the ErrorList.
129136
func ValidatePodLogOptions(opts *v1.PodLogOptions) field.ErrorList {
@@ -142,6 +149,15 @@ func ValidatePodLogOptions(opts *v1.PodLogOptions) field.ErrorList {
142149
allErrs = append(allErrs, field.Invalid(field.NewPath("sinceSeconds"), *opts.SinceSeconds, "must be greater than 0"))
143150
}
144151
}
152+
// opts.Stream can be nil because defaulting might not apply if no URL params are provided.
153+
if opts.Stream != nil {
154+
if !validLogStreams.Has(*opts.Stream) {
155+
allErrs = append(allErrs, field.NotSupported(field.NewPath("stream"), *opts.Stream, validLogStreams.UnsortedList()))
156+
}
157+
if *opts.Stream != v1.LogStreamAll && opts.TailLines != nil {
158+
allErrs = append(allErrs, field.Forbidden(field.NewPath(""), "`tailLines` and specific `stream` are mutually exclusive for now"))
159+
}
160+
}
145161
return allErrs
146162
}
147163

pkg/apis/core/v1/validation/validation_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2626
"k8s.io/apimachinery/pkg/util/sets"
2727
"k8s.io/apimachinery/pkg/util/validation/field"
28+
2829
"k8s.io/kubernetes/pkg/apis/core"
2930
)
3031

@@ -216,6 +217,10 @@ func TestValidatePodLogOptions(t *testing.T) {
216217
sinceSecondsGreaterThan1 = int64(10)
217218
sinceSecondsLessThan1 = int64(0)
218219
timestamp = metav1.Now()
220+
stdoutStream = v1.LogStreamStdout
221+
stderrStream = v1.LogStreamStderr
222+
allStream = v1.LogStreamAll
223+
invalidStream = "invalid"
219224
)
220225

221226
successCase := []struct {
@@ -252,6 +257,24 @@ func TestValidatePodLogOptions(t *testing.T) {
252257
TailLines: &positiveLine,
253258
SinceSeconds: &sinceSecondsGreaterThan1,
254259
},
260+
}, {
261+
name: "PodLogOptions with stdout Stream",
262+
podLogOptions: v1.PodLogOptions{
263+
Stream: &stdoutStream,
264+
},
265+
}, {
266+
name: "PodLogOptions with stderr Stream and Follow",
267+
podLogOptions: v1.PodLogOptions{
268+
Stream: &stderrStream,
269+
Follow: true,
270+
},
271+
}, {
272+
name: "PodLogOptions with All Stream, TailLines and LimitBytes",
273+
podLogOptions: v1.PodLogOptions{
274+
Stream: &allStream,
275+
TailLines: &positiveLine,
276+
LimitBytes: &limitBytesGreaterThan1,
277+
},
255278
}}
256279
for _, tc := range successCase {
257280
t.Run(tc.name, func(t *testing.T) {
@@ -293,6 +316,23 @@ func TestValidatePodLogOptions(t *testing.T) {
293316
SinceSeconds: &sinceSecondsGreaterThan1,
294317
SinceTime: &timestamp,
295318
},
319+
}, {
320+
name: "Invalid podLogOptions with invalid Stream",
321+
podLogOptions: v1.PodLogOptions{
322+
Stream: &invalidStream,
323+
},
324+
}, {
325+
name: "Invalid podLogOptions with stdout Stream and TailLines set",
326+
podLogOptions: v1.PodLogOptions{
327+
Stream: &stdoutStream,
328+
TailLines: &positiveLine,
329+
},
330+
}, {
331+
name: "Invalid podLogOptions with stderr Stream and TailLines set",
332+
podLogOptions: v1.PodLogOptions{
333+
Stream: &stderrStream,
334+
TailLines: &positiveLine,
335+
},
296336
}}
297337
for _, tc := range errorCase {
298338
t.Run(tc.name, func(t *testing.T) {

0 commit comments

Comments
 (0)