From bda7e28bbb749c1cb5378a423d2879c63c0fd5a8 Mon Sep 17 00:00:00 2001 From: Davanum Srinivas Date: Mon, 12 May 2025 09:29:22 -0400 Subject: [PATCH] Avoid encoding in LogResponseObject when we are not going to use it Signed-off-by: Davanum Srinivas Kubernetes-commit: e418ee3a92ca6c670d26f775b0f669e8a5fe233c --- pkg/audit/request.go | 2 +- pkg/audit/request_log_test.go | 259 ++++++++++++++++++++++++++++++++++ 2 files changed, 260 insertions(+), 1 deletion(-) create mode 100644 pkg/audit/request_log_test.go diff --git a/pkg/audit/request.go b/pkg/audit/request.go index 60b69b0b2..705d11e88 100644 --- a/pkg/audit/request.go +++ b/pkg/audit/request.go @@ -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 } diff --git a/pkg/audit/request_log_test.go b/pkg/audit/request_log_test.go new file mode 100644 index 000000000..b41450db2 --- /dev/null +++ b/pkg/audit/request_log_test.go @@ -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 +}