Skip to content
This repository was archived by the owner on Dec 4, 2025. It is now read-only.

Commit 472beb8

Browse files
authored
fix: return empty list instead of nil to prevent panic. Fixes #25189 (#25192) (#794)
Signed-off-by: Ivan Pedersen <[email protected]>
1 parent 13d5172 commit 472beb8

File tree

2 files changed

+113
-0
lines changed

2 files changed

+113
-0
lines changed

pkg/cache/cluster.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,10 @@ func (c *clusterCache) listResources(ctx context.Context, resClient dynamic.Reso
606606
retryCount++
607607
c.log.Info(fmt.Sprintf("Error while listing resources: %v (try %d/%d)", ierr, retryCount, c.listRetryLimit))
608608
}
609+
// Ensure res is never nil even when there's an error
610+
if res == nil {
611+
res = &unstructured.UnstructuredList{}
612+
}
609613
//nolint:wrapcheck // wrap outside the retry
610614
return ierr
611615
}

pkg/cache/cluster_nil_fix_test.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package cache
2+
3+
import (
4+
"context"
5+
"errors"
6+
"testing"
7+
8+
"golang.org/x/sync/semaphore"
9+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
11+
"k8s.io/apimachinery/pkg/runtime"
12+
"k8s.io/apimachinery/pkg/types"
13+
"k8s.io/apimachinery/pkg/watch"
14+
"k8s.io/client-go/dynamic"
15+
"k8s.io/client-go/tools/pager"
16+
"k8s.io/klog/v2/textlogger"
17+
)
18+
19+
// mockResourceInterface simulates a dynamic resource interface that returns (nil, error)
20+
type mockResourceInterface struct{}
21+
22+
func (m *mockResourceInterface) Create(ctx context.Context, obj *unstructured.Unstructured, options metav1.CreateOptions, subresources ...string) (*unstructured.Unstructured, error) {
23+
return nil, errors.New("not implemented")
24+
}
25+
26+
func (m *mockResourceInterface) Update(ctx context.Context, obj *unstructured.Unstructured, options metav1.UpdateOptions, subresources ...string) (*unstructured.Unstructured, error) {
27+
return nil, errors.New("not implemented")
28+
}
29+
30+
func (m *mockResourceInterface) UpdateStatus(ctx context.Context, obj *unstructured.Unstructured, options metav1.UpdateOptions) (*unstructured.Unstructured, error) {
31+
return nil, errors.New("not implemented")
32+
}
33+
34+
func (m *mockResourceInterface) Delete(ctx context.Context, name string, options metav1.DeleteOptions, subresources ...string) error {
35+
return errors.New("not implemented")
36+
}
37+
38+
func (m *mockResourceInterface) DeleteCollection(ctx context.Context, options metav1.DeleteOptions, listOptions metav1.ListOptions) error {
39+
return errors.New("not implemented")
40+
}
41+
42+
func (m *mockResourceInterface) Get(ctx context.Context, name string, options metav1.GetOptions, subresources ...string) (*unstructured.Unstructured, error) {
43+
return nil, errors.New("not implemented")
44+
}
45+
46+
func (m *mockResourceInterface) List(ctx context.Context, opts metav1.ListOptions) (*unstructured.UnstructuredList, error) {
47+
// This simulates the problematic behavior that caused the original panic:
48+
// returning (nil, error) instead of (emptyList, error)
49+
return nil, errors.New("simulated list failure")
50+
}
51+
52+
func (m *mockResourceInterface) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) {
53+
return nil, errors.New("not implemented")
54+
}
55+
56+
func (m *mockResourceInterface) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, options metav1.PatchOptions, subresources ...string) (*unstructured.Unstructured, error) {
57+
return nil, errors.New("not implemented")
58+
}
59+
60+
func (m *mockResourceInterface) Apply(ctx context.Context, name string, obj *unstructured.Unstructured, options metav1.ApplyOptions, subresources ...string) (*unstructured.Unstructured, error) {
61+
return nil, errors.New("not implemented")
62+
}
63+
64+
func (m *mockResourceInterface) ApplyStatus(ctx context.Context, name string, obj *unstructured.Unstructured, options metav1.ApplyOptions) (*unstructured.Unstructured, error) {
65+
return nil, errors.New("not implemented")
66+
}
67+
68+
func (m *mockResourceInterface) Namespace(string) dynamic.ResourceInterface {
69+
return m
70+
}
71+
72+
// TestListResourcesNilPointerFix tests that our fix prevents panic when
73+
// resClient.List() returns (nil, error)
74+
func TestListResourcesNilPointerFix(t *testing.T) {
75+
// Create a cluster cache with proper configuration
76+
cache := &clusterCache{
77+
listSemaphore: semaphore.NewWeighted(1),
78+
listPageSize: 100,
79+
listPageBufferSize: 1,
80+
listRetryLimit: 1,
81+
listRetryUseBackoff: false,
82+
listRetryFunc: ListRetryFuncNever,
83+
log: textlogger.NewLogger(textlogger.NewConfig()),
84+
}
85+
86+
// Use our mock that returns (nil, error)
87+
mockClient := &mockResourceInterface{}
88+
89+
// This should not panic even though mockClient.List() returns (nil, error)
90+
_, err := cache.listResources(context.Background(), mockClient, func(listPager *pager.ListPager) error {
91+
// The fix ensures the pager receives a non-nil UnstructuredList
92+
// preventing panic in GetContinue()
93+
return listPager.EachListItem(context.Background(), metav1.ListOptions{}, func(obj runtime.Object) error {
94+
return nil
95+
})
96+
})
97+
98+
// We expect an error (wrapped), but no panic should occur
99+
if err == nil {
100+
t.Fatal("Expected error from listResources due to simulated failure")
101+
}
102+
103+
// Check that the error is properly wrapped
104+
if err.Error() != "failed to list resources: simulated list failure" {
105+
t.Fatalf("Unexpected error message: %v", err.Error())
106+
}
107+
108+
t.Log("Test passed: no panic occurred despite (nil, error) from resClient.List()")
109+
}

0 commit comments

Comments
 (0)