Avoid encoding in LogResponseObject when we are not going to use it

Signed-off-by: Davanum Srinivas <davanum@gmail.com>

Kubernetes-commit: e418ee3a92ca6c670d26f775b0f669e8a5fe233c
This commit is contained in:
Davanum Srinivas 2025-05-12 09:29:22 -04:00 committed by Kubernetes Publisher
parent 9b509bf53b
commit bda7e28bbb
2 changed files with 260 additions and 1 deletions

View File

@ -171,7 +171,7 @@ func LogRequestPatch(ctx context.Context, patch []byte) {
// will be converted to the given gv.
func LogResponseObject(ctx context.Context, obj runtime.Object, gv schema.GroupVersion, s runtime.NegotiatedSerializer) {
ac := AuditContextFrom(WithAuditContext(ctx))
if ac.GetEventLevel().Less(auditinternal.LevelMetadata) {
if ac.GetEventLevel().Less(auditinternal.LevelRequestResponse) {
return
}

View File

@ -0,0 +1,259 @@
/*
Copyright 2025 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package audit
import (
"context"
"io"
"strings"
"testing"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
auditinternal "k8s.io/apiserver/pkg/apis/audit"
)
func TestLogResponseObjectWithPod(t *testing.T) {
testPod := &corev1.Pod{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Pod",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test-pod",
Namespace: "test-namespace",
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "test-container",
Image: "test-image",
},
},
},
}
scheme := runtime.NewScheme()
if err := corev1.AddToScheme(scheme); err != nil {
t.Fatalf("Failed to add core/v1 to scheme: %v", err)
}
codecs := serializer.NewCodecFactory(scheme)
negotiatedSerializer := codecs.WithoutConversion()
// Create audit context with RequestResponse level
ctx := WithAuditContext(context.Background())
ac := AuditContextFrom(ctx)
captureSink := &capturingAuditSink{}
if err := ac.Init(RequestAuditConfig{Level: auditinternal.LevelRequestResponse}, captureSink); err != nil {
t.Fatalf("Failed to initialize audit context: %v", err)
}
LogResponseObject(ctx, testPod, schema.GroupVersion{Group: "", Version: "v1"}, negotiatedSerializer)
ac.ProcessEventStage(ctx, auditinternal.StageResponseComplete)
if len(captureSink.events) != 1 {
t.Fatalf("Expected one audit event to be captured, got %d", len(captureSink.events))
}
event := captureSink.events[0]
if event.ResponseObject == nil {
t.Fatal("Expected ResponseObject to be set, but it was nil")
}
if event.ResponseObject.ContentType != runtime.ContentTypeJSON {
t.Errorf("Expected ContentType to be %q, got %q", runtime.ContentTypeJSON, event.ResponseObject.ContentType)
}
if len(event.ResponseObject.Raw) == 0 {
t.Error("Expected ResponseObject.Raw to contain data, but it was empty")
}
responseJSON := string(event.ResponseObject.Raw)
expectedFields := []string{"test-pod", "test-namespace", "test-container", "test-image"}
for _, field := range expectedFields {
if !strings.Contains(responseJSON, field) {
t.Errorf("Response should contain %q but didn't. Response: %s", field, responseJSON)
}
}
if event.ResponseStatus != nil {
t.Errorf("Expected ResponseStatus to be nil for regular object, got: %+v", event.ResponseStatus)
}
}
func TestLogResponseObjectWithStatus(t *testing.T) {
// Create a status object to test ResponseStatus handling
testStatus := &metav1.Status{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Status",
},
Status: "Success",
Message: "Test status message",
Reason: "TestReason",
Code: 200,
}
scheme := runtime.NewScheme()
err := metav1.AddMetaToScheme(scheme)
if err != nil {
t.Fatalf("Failed to add meta to scheme: %v", err)
}
scheme.AddKnownTypes(schema.GroupVersion{Version: "v1"}, &metav1.Status{})
codecs := serializer.NewCodecFactory(scheme)
negotiatedSerializer := codecs.WithoutConversion()
ctx := WithAuditContext(context.Background())
ac := AuditContextFrom(ctx)
captureSink := &capturingAuditSink{}
if err := ac.Init(RequestAuditConfig{Level: auditinternal.LevelRequestResponse}, captureSink); err != nil {
t.Fatalf("Failed to initialize audit context: %v", err)
}
LogResponseObject(ctx, testStatus, schema.GroupVersion{Group: "", Version: "v1"}, negotiatedSerializer)
ac.ProcessEventStage(ctx, auditinternal.StageResponseComplete)
if len(captureSink.events) != 1 {
t.Fatalf("Expected one audit event to be captured, got %d", len(captureSink.events))
}
event := captureSink.events[0]
if event.ResponseObject == nil {
t.Fatal("Expected ResponseObject to be set, but it was nil")
}
if event.ResponseStatus == nil {
t.Fatal("Expected ResponseStatus to be set for Status object, but it was nil")
}
if event.ResponseStatus.Status != "Success" {
t.Errorf("Expected ResponseStatus.Status to be 'Success', got %q", event.ResponseStatus.Status)
}
if event.ResponseStatus.Message != "Test status message" {
t.Errorf("Expected ResponseStatus.Message to be 'Test status message', got %q", event.ResponseStatus.Message)
}
if event.ResponseStatus.Reason != "TestReason" {
t.Errorf("Expected ResponseStatus.Reason to be 'TestReason', got %q", event.ResponseStatus.Reason)
}
if event.ResponseStatus.Code != 200 {
t.Errorf("Expected ResponseStatus.Code to be 200, got %d", event.ResponseStatus.Code)
}
}
func TestLogResponseObjectLevelCheck(t *testing.T) {
testCases := []struct {
name string
level auditinternal.Level
shouldEncode bool
}{
{
name: "None level should not encode",
level: auditinternal.LevelNone,
shouldEncode: false,
},
{
name: "Metadata level should not encode",
level: auditinternal.LevelMetadata,
shouldEncode: false,
},
{
name: "Request level should not encode",
level: auditinternal.LevelRequest,
shouldEncode: false,
},
{
name: "RequestResponse level should encode",
level: auditinternal.LevelRequestResponse,
shouldEncode: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Create a test object
testObj := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "test-pod",
Namespace: "test-namespace",
},
}
// Create audit context with the specified level
ctx := WithAuditContext(context.Background())
ac := AuditContextFrom(ctx)
ac.Init(RequestAuditConfig{Level: tc.level}, nil)
// Create a mock serializer that tracks if encoding was attempted
mockSerializer := &mockNegotiatedSerializer{}
// Call the function under test
LogResponseObject(ctx, testObj, schema.GroupVersion{Group: "", Version: "v1"}, mockSerializer)
// Check if encoding was attempted as expected
if mockSerializer.encodeCalled != tc.shouldEncode {
t.Errorf("Expected encoding to be called: %v, but got: %v", tc.shouldEncode, mockSerializer.encodeCalled)
}
})
}
}
type mockNegotiatedSerializer struct {
encodeCalled bool
}
func (m *mockNegotiatedSerializer) SupportedMediaTypes() []runtime.SerializerInfo {
return []runtime.SerializerInfo{
{
MediaType: runtime.ContentTypeJSON,
EncodesAsText: true,
Serializer: nil,
PrettySerializer: nil,
StreamSerializer: nil,
},
}
}
func (m *mockNegotiatedSerializer) EncoderForVersion(serializer runtime.Encoder, gv runtime.GroupVersioner) runtime.Encoder {
m.encodeCalled = true
return &mockEncoder{}
}
func (m *mockNegotiatedSerializer) DecoderToVersion(serializer runtime.Decoder, gv runtime.GroupVersioner) runtime.Decoder {
return nil
}
type mockEncoder struct{}
func (e *mockEncoder) Encode(obj runtime.Object, w io.Writer) error {
return nil
}
func (e *mockEncoder) Identifier() runtime.Identifier {
return runtime.Identifier("mock")
}
type capturingAuditSink struct {
events []*auditinternal.Event
}
func (s *capturingAuditSink) ProcessEvents(events ...*auditinternal.Event) bool {
for _, event := range events {
eventCopy := event.DeepCopy()
s.events = append(s.events, eventCopy)
}
return true
}