Skip to content

Commit 30f86a7

Browse files
k8s-ci-robottjungblu
authored andcommitted
UPSTREAM: 131725: Avoid encoding in LogResponseObject when we are not going to use it
Merge pull request kubernetes#131725 from dims/avoid-encoding-in-log-response-object-when-we-dont-need-it
1 parent 9cba7c9 commit 30f86a7

File tree

2 files changed

+366
-2
lines changed

2 files changed

+366
-2
lines changed

staging/src/k8s.io/apiserver/pkg/audit/request.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,12 +171,14 @@ func LogRequestPatch(ctx context.Context, patch []byte) {
171171
// will be converted to the given gv.
172172
func LogResponseObject(ctx context.Context, obj runtime.Object, gv schema.GroupVersion, s runtime.NegotiatedSerializer) {
173173
ac := AuditContextFrom(WithAuditContext(ctx))
174+
status, _ := obj.(*metav1.Status)
174175
if ac.GetEventLevel().Less(auditinternal.LevelMetadata) {
175176
return
177+
} else if ac.GetEventLevel().Less(auditinternal.LevelRequestResponse) {
178+
ac.LogResponseObject(status, nil)
179+
return
176180
}
177181

178-
status, _ := obj.(*metav1.Status)
179-
180182
if shouldOmitManagedFields(ac) {
181183
copy, ok, err := copyWithoutManagedFields(obj)
182184
if err != nil {
Lines changed: 362 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,362 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package audit
18+
19+
import (
20+
"context"
21+
"io"
22+
"strings"
23+
"testing"
24+
25+
corev1 "k8s.io/api/core/v1"
26+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27+
"k8s.io/apimachinery/pkg/runtime"
28+
"k8s.io/apimachinery/pkg/runtime/schema"
29+
"k8s.io/apimachinery/pkg/runtime/serializer"
30+
auditinternal "k8s.io/apiserver/pkg/apis/audit"
31+
)
32+
33+
func TestLogResponseObjectWithPod(t *testing.T) {
34+
testPod := &corev1.Pod{
35+
TypeMeta: metav1.TypeMeta{
36+
APIVersion: "v1",
37+
Kind: "Pod",
38+
},
39+
ObjectMeta: metav1.ObjectMeta{
40+
Name: "test-pod",
41+
Namespace: "test-namespace",
42+
},
43+
Spec: corev1.PodSpec{
44+
Containers: []corev1.Container{
45+
{
46+
Name: "test-container",
47+
Image: "test-image",
48+
},
49+
},
50+
},
51+
}
52+
53+
scheme := runtime.NewScheme()
54+
if err := corev1.AddToScheme(scheme); err != nil {
55+
t.Fatalf("Failed to add core/v1 to scheme: %v", err)
56+
}
57+
codecs := serializer.NewCodecFactory(scheme)
58+
negotiatedSerializer := codecs.WithoutConversion()
59+
60+
// Create audit context with RequestResponse level
61+
ctx := WithAuditContext(context.Background())
62+
ac := AuditContextFrom(ctx)
63+
64+
captureSink := &capturingAuditSink{}
65+
if err := ac.Init(RequestAuditConfig{Level: auditinternal.LevelRequestResponse}, captureSink); err != nil {
66+
t.Fatalf("Failed to initialize audit context: %v", err)
67+
}
68+
69+
LogResponseObject(ctx, testPod, schema.GroupVersion{Group: "", Version: "v1"}, negotiatedSerializer)
70+
ac.ProcessEventStage(ctx, auditinternal.StageResponseComplete)
71+
72+
if len(captureSink.events) != 1 {
73+
t.Fatalf("Expected one audit event to be captured, got %d", len(captureSink.events))
74+
}
75+
event := captureSink.events[0]
76+
if event.ResponseObject == nil {
77+
t.Fatal("Expected ResponseObject to be set, but it was nil")
78+
}
79+
if event.ResponseObject.ContentType != runtime.ContentTypeJSON {
80+
t.Errorf("Expected ContentType to be %q, got %q", runtime.ContentTypeJSON, event.ResponseObject.ContentType)
81+
}
82+
if len(event.ResponseObject.Raw) == 0 {
83+
t.Error("Expected ResponseObject.Raw to contain data, but it was empty")
84+
}
85+
86+
responseJSON := string(event.ResponseObject.Raw)
87+
expectedFields := []string{"test-pod", "test-namespace", "test-container", "test-image"}
88+
for _, field := range expectedFields {
89+
if !strings.Contains(responseJSON, field) {
90+
t.Errorf("Response should contain %q but didn't. Response: %s", field, responseJSON)
91+
}
92+
}
93+
94+
if event.ResponseStatus != nil {
95+
t.Errorf("Expected ResponseStatus to be nil for regular object, got: %+v", event.ResponseStatus)
96+
}
97+
}
98+
99+
func TestLogResponseObjectWithStatus(t *testing.T) {
100+
testCases := []struct {
101+
name string
102+
level auditinternal.Level
103+
status *metav1.Status
104+
shouldEncode bool
105+
expectResponseObj bool
106+
expectStatusFields bool
107+
}{
108+
{
109+
name: "RequestResponse level should encode and log status fields",
110+
level: auditinternal.LevelRequestResponse,
111+
status: &metav1.Status{Status: "Success", Message: "Test message", Code: 200},
112+
shouldEncode: true,
113+
expectResponseObj: true,
114+
expectStatusFields: true,
115+
},
116+
{
117+
name: "Metadata level should log status fields without encoding",
118+
level: auditinternal.LevelMetadata,
119+
status: &metav1.Status{Status: "Success", Message: "Test message", Code: 200},
120+
shouldEncode: false,
121+
expectResponseObj: false,
122+
expectStatusFields: true,
123+
},
124+
}
125+
126+
for _, tc := range testCases {
127+
t.Run(tc.name, func(t *testing.T) {
128+
scheme := runtime.NewScheme()
129+
if err := metav1.AddMetaToScheme(scheme); err != nil {
130+
t.Fatalf("Failed to add meta to scheme: %v", err)
131+
}
132+
scheme.AddKnownTypes(schema.GroupVersion{Version: "v1"}, &metav1.Status{})
133+
codecs := serializer.NewCodecFactory(scheme)
134+
negotiatedSerializer := codecs.WithoutConversion()
135+
136+
ctx := WithAuditContext(context.Background())
137+
ac := AuditContextFrom(ctx)
138+
139+
captureSink := &capturingAuditSink{}
140+
if err := ac.Init(RequestAuditConfig{Level: tc.level}, captureSink); err != nil {
141+
t.Fatalf("Failed to initialize audit context: %v", err)
142+
}
143+
144+
LogResponseObject(ctx, tc.status, schema.GroupVersion{Group: "", Version: "v1"}, negotiatedSerializer)
145+
ac.ProcessEventStage(ctx, auditinternal.StageResponseComplete)
146+
147+
if len(captureSink.events) != 1 {
148+
t.Fatalf("Expected one audit event to be captured, got %d", len(captureSink.events))
149+
}
150+
event := captureSink.events[0]
151+
152+
if tc.expectResponseObj {
153+
if event.ResponseObject == nil {
154+
t.Error("Expected ResponseObject to be set, but it was nil")
155+
}
156+
} else {
157+
if event.ResponseObject != nil {
158+
t.Error("Expected ResponseObject to be nil")
159+
}
160+
}
161+
162+
if tc.expectStatusFields {
163+
if event.ResponseStatus == nil {
164+
t.Fatal("Expected ResponseStatus to be set, but it was nil")
165+
}
166+
if event.ResponseStatus.Status != tc.status.Status {
167+
t.Errorf("Expected ResponseStatus.Status to be %q, got %q", tc.status.Status, event.ResponseStatus.Status)
168+
}
169+
if event.ResponseStatus.Message != tc.status.Message {
170+
t.Errorf("Expected ResponseStatus.Message to be %q, got %q", tc.status.Message, event.ResponseStatus.Message)
171+
}
172+
if event.ResponseStatus.Code != tc.status.Code {
173+
t.Errorf("Expected ResponseStatus.Code to be %d, got %d", tc.status.Code, event.ResponseStatus.Code)
174+
}
175+
} else if event.ResponseStatus != nil {
176+
t.Error("Expected ResponseStatus to be nil")
177+
}
178+
})
179+
}
180+
}
181+
182+
func TestLogResponseObjectLevelCheck(t *testing.T) {
183+
testCases := []struct {
184+
name string
185+
level auditinternal.Level
186+
obj runtime.Object
187+
shouldEncode bool
188+
expectResponseObj bool
189+
expectStatusFields bool
190+
}{
191+
{
192+
name: "None level should not encode or log anything",
193+
level: auditinternal.LevelNone,
194+
obj: &corev1.Pod{},
195+
shouldEncode: false,
196+
expectResponseObj: false,
197+
expectStatusFields: false,
198+
},
199+
{
200+
name: "Metadata level should not encode or log anything",
201+
level: auditinternal.LevelMetadata,
202+
obj: &corev1.Pod{},
203+
shouldEncode: false,
204+
expectResponseObj: false,
205+
expectStatusFields: false,
206+
},
207+
{
208+
name: "Metadata level with Status should log status fields without encoding",
209+
level: auditinternal.LevelMetadata,
210+
obj: &metav1.Status{
211+
Status: "Success",
212+
Message: "Test message",
213+
Code: 200,
214+
},
215+
shouldEncode: false,
216+
expectResponseObj: false,
217+
expectStatusFields: true,
218+
},
219+
{
220+
name: "Request level with Pod should not encode or log",
221+
level: auditinternal.LevelRequest,
222+
obj: &corev1.Pod{},
223+
shouldEncode: false,
224+
expectResponseObj: false,
225+
expectStatusFields: false,
226+
},
227+
{
228+
name: "Request level with Status should log status fields without encoding",
229+
level: auditinternal.LevelRequest,
230+
obj: &metav1.Status{
231+
Status: "Success",
232+
Message: "Test message",
233+
Code: 200,
234+
},
235+
shouldEncode: false,
236+
expectResponseObj: false,
237+
expectStatusFields: true,
238+
},
239+
{
240+
name: "RequestResponse level with Pod should encode",
241+
level: auditinternal.LevelRequestResponse,
242+
obj: &corev1.Pod{},
243+
shouldEncode: true,
244+
expectResponseObj: true,
245+
expectStatusFields: false,
246+
},
247+
{
248+
name: "RequestResponse level with Status should encode and log status fields",
249+
level: auditinternal.LevelRequestResponse,
250+
obj: &metav1.Status{
251+
Status: "Success",
252+
Message: "Test message",
253+
Code: 200,
254+
},
255+
shouldEncode: true,
256+
expectResponseObj: true,
257+
expectStatusFields: true,
258+
},
259+
}
260+
261+
for _, tc := range testCases {
262+
t.Run(tc.name, func(t *testing.T) {
263+
ctx := WithAuditContext(context.Background())
264+
ac := AuditContextFrom(ctx)
265+
266+
captureSink := &capturingAuditSink{}
267+
if err := ac.Init(RequestAuditConfig{Level: tc.level}, captureSink); err != nil {
268+
t.Fatalf("Failed to initialize audit context: %v", err)
269+
}
270+
271+
mockSerializer := &mockNegotiatedSerializer{}
272+
LogResponseObject(ctx, tc.obj, schema.GroupVersion{Group: "", Version: "v1"}, mockSerializer)
273+
ac.ProcessEventStage(ctx, auditinternal.StageResponseComplete)
274+
275+
if mockSerializer.encodeCalled != tc.shouldEncode {
276+
t.Errorf("Expected encoding to be called: %v, but got: %v", tc.shouldEncode, mockSerializer.encodeCalled)
277+
}
278+
279+
if len(captureSink.events) != 1 {
280+
t.Fatalf("Expected one audit event to be captured, got %d", len(captureSink.events))
281+
}
282+
event := captureSink.events[0]
283+
284+
if tc.expectResponseObj {
285+
if event.ResponseObject == nil {
286+
t.Error("Expected ResponseObject to be set, but it was nil")
287+
}
288+
} else {
289+
if event.ResponseObject != nil {
290+
t.Error("Expected ResponseObject to be nil")
291+
}
292+
}
293+
294+
// Check ResponseStatus for Status objects
295+
status, isStatus := tc.obj.(*metav1.Status)
296+
if isStatus && tc.expectStatusFields {
297+
if event.ResponseStatus == nil {
298+
t.Error("Expected ResponseStatus to be set for Status object, but it was nil")
299+
} else {
300+
if event.ResponseStatus.Status != status.Status {
301+
t.Errorf("Expected ResponseStatus.Status to be %q, got %q", status.Status, event.ResponseStatus.Status)
302+
}
303+
if event.ResponseStatus.Message != status.Message {
304+
t.Errorf("Expected ResponseStatus.Message to be %q, got %q", status.Message, event.ResponseStatus.Message)
305+
}
306+
if event.ResponseStatus.Code != status.Code {
307+
t.Errorf("Expected ResponseStatus.Code to be %d, got %d", status.Code, event.ResponseStatus.Code)
308+
}
309+
}
310+
} else if event.ResponseStatus != nil {
311+
t.Error("Expected ResponseStatus to be nil")
312+
}
313+
})
314+
}
315+
}
316+
317+
type mockNegotiatedSerializer struct {
318+
encodeCalled bool
319+
}
320+
321+
func (m *mockNegotiatedSerializer) SupportedMediaTypes() []runtime.SerializerInfo {
322+
return []runtime.SerializerInfo{
323+
{
324+
MediaType: runtime.ContentTypeJSON,
325+
EncodesAsText: true,
326+
Serializer: nil,
327+
PrettySerializer: nil,
328+
StreamSerializer: nil,
329+
},
330+
}
331+
}
332+
333+
func (m *mockNegotiatedSerializer) EncoderForVersion(serializer runtime.Encoder, gv runtime.GroupVersioner) runtime.Encoder {
334+
m.encodeCalled = true
335+
return &mockEncoder{}
336+
}
337+
338+
func (m *mockNegotiatedSerializer) DecoderToVersion(serializer runtime.Decoder, gv runtime.GroupVersioner) runtime.Decoder {
339+
return nil
340+
}
341+
342+
type mockEncoder struct{}
343+
344+
func (e *mockEncoder) Encode(obj runtime.Object, w io.Writer) error {
345+
return nil
346+
}
347+
348+
func (e *mockEncoder) Identifier() runtime.Identifier {
349+
return runtime.Identifier("mock")
350+
}
351+
352+
type capturingAuditSink struct {
353+
events []*auditinternal.Event
354+
}
355+
356+
func (s *capturingAuditSink) ProcessEvents(events ...*auditinternal.Event) bool {
357+
for _, event := range events {
358+
eventCopy := event.DeepCopy()
359+
s.events = append(s.events, eventCopy)
360+
}
361+
return true
362+
}

0 commit comments

Comments
 (0)