github.com/castai/kvisor@v1.7.1-0.20240516114728-b3572a2607b5/cmd/controller/state/delta/controller_test.go (about) 1 package delta 2 3 import ( 4 "context" 5 "errors" 6 "sort" 7 "sync" 8 "testing" 9 "time" 10 11 "github.com/castai/kvisor/api/v1/runtime" 12 "github.com/castai/kvisor/cmd/controller/kube" 13 "github.com/castai/kvisor/pkg/logging" 14 "github.com/samber/lo" 15 "github.com/stretchr/testify/require" 16 "go.uber.org/goleak" 17 "google.golang.org/grpc" 18 "google.golang.org/grpc/metadata" 19 "google.golang.org/protobuf/encoding/protojson" 20 appsv1 "k8s.io/api/apps/v1" 21 corev1 "k8s.io/api/core/v1" 22 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 "k8s.io/apimachinery/pkg/types" 24 25 castaipb "github.com/castai/kvisor/api/v1/runtime" 26 ) 27 28 func TestController(t *testing.T) { 29 defer goleak.VerifyNone(t) 30 31 ctx := context.Background() 32 log := logging.NewTestLog() 33 34 dep1 := &appsv1.Deployment{ 35 TypeMeta: metav1.TypeMeta{Kind: "Deployment", APIVersion: "v1"}, 36 ObjectMeta: metav1.ObjectMeta{ 37 Name: "nginx-1", 38 Namespace: "default", 39 UID: types.UID("111b56a9-ab5e-4a35-93af-f092e2f63011"), 40 OwnerReferences: []metav1.OwnerReference{ 41 { 42 UID: types.UID("owner"), 43 APIVersion: "v1", 44 Kind: "Node", 45 Controller: lo.ToPtr(true), 46 Name: "node1", 47 }, 48 }, 49 Labels: map[string]string{"subscriber": "test"}, 50 }, 51 Spec: appsv1.DeploymentSpec{ 52 Template: corev1.PodTemplateSpec{ 53 Spec: corev1.PodSpec{ 54 NodeName: "n1", 55 Containers: []corev1.Container{ 56 { 57 Name: "nginx", 58 Image: "nginx:1.23", 59 }, 60 }, 61 }, 62 }, 63 }, 64 Status: appsv1.DeploymentStatus{ 65 Replicas: 1, 66 }, 67 } 68 69 assertDelta := func(t *testing.T, delta *castaipb.KubernetesDeltaItem, event castaipb.KubernetesDeltaItemEvent, initial bool) { 70 t.Helper() 71 r := require.New(t) 72 podUID := "111b56a9-ab5e-4a35-93af-f092e2f63011" 73 expected, err := protojson.MarshalOptions{Multiline: true}.Marshal(&castaipb.KubernetesDeltaItem{ 74 Event: event, 75 ObjectUid: podUID, 76 ObjectName: "nginx-1", 77 ObjectNamespace: "default", 78 ObjectKind: "Deployment", 79 ObjectApiVersion: "v1", 80 ObjectCreatedAt: delta.ObjectCreatedAt, 81 ObjectLabels: map[string]string{"subscriber": "test"}, 82 ObjectContainers: []*castaipb.Container{ 83 { 84 Name: "nginx", 85 ImageName: "nginx:1.23", 86 }, 87 }, 88 ObjectStatus: []byte(`{"replicas":1}`), 89 ObjectOwnerUid: "111b56a9-ab5e-4a35-93af-f092e2f63011", 90 ObjectSpec: []byte(`{"selector":null,"template":{"metadata":{"creationTimestamp":null},"spec":{"containers":[{"name":"nginx","image":"nginx:1.23","resources":{}}],"nodeName":"n1"}},"strategy":{}}`), 91 }) 92 r.NoError(err) 93 94 actual, err := protojson.MarshalOptions{Multiline: true}.Marshal(delta) 95 r.NoError(err) 96 97 r.Equal(string(expected), string(actual)) 98 } 99 100 t.Run("send add event", func(t *testing.T) { 101 r := require.New(t) 102 103 client := newMockClient() 104 ctrl := newTestController(log, client) 105 ctrl.castaiClient = client 106 ctrl.OnAdd(dep1) 107 108 ctx, cancel := context.WithTimeout(ctx, 30*time.Millisecond) 109 defer cancel() 110 err := ctrl.Run(ctx) 111 r.ErrorIs(err, context.DeadlineExceeded) 112 r.Len(client.deltas, 1) 113 assertDelta(t, client.deltas[0], castaipb.KubernetesDeltaItemEvent_DELTA_ADD, true) 114 }) 115 116 t.Run("send update event", func(t *testing.T) { 117 r := require.New(t) 118 119 client := newMockClient() 120 ctrl := newTestController(log, client) 121 ctrl.castaiClient = client 122 123 ctrl.OnAdd(dep1) 124 ctrl.OnUpdate(dep1) 125 126 ctx, cancel := context.WithTimeout(ctx, 30*time.Millisecond) 127 defer cancel() 128 err := ctrl.Run(ctx) 129 r.ErrorIs(err, context.DeadlineExceeded) 130 r.Len(client.deltas, 1) 131 assertDelta(t, client.deltas[0], castaipb.KubernetesDeltaItemEvent_DELTA_UPDATE, true) 132 }) 133 134 t.Run("send delete event", func(t *testing.T) { 135 r := require.New(t) 136 client := newMockClient() 137 ctrl := newTestController(log, client) 138 ctrl.castaiClient = client 139 ctrl.OnAdd(dep1) 140 ctrl.OnUpdate(dep1) 141 ctrl.OnDelete(dep1) 142 143 ctx, cancel := context.WithTimeout(ctx, 30*time.Millisecond) 144 defer cancel() 145 err := ctrl.Run(ctx) 146 r.ErrorIs(err, context.DeadlineExceeded) 147 r.Len(client.deltas, 1) 148 assertDelta(t, client.deltas[0], castaipb.KubernetesDeltaItemEvent_DELTA_REMOVE, true) 149 }) 150 151 t.Run("second event does not set full snapshot flag", func(t *testing.T) { 152 r := require.New(t) 153 client := newMockClient() 154 ctrl := newTestController(log, client) 155 ctrl.castaiClient = client 156 ctrl.OnAdd(dep1) 157 158 go func() { 159 time.Sleep(time.Millisecond * 20) 160 ctrl.OnAdd(dep1) 161 }() 162 163 ctx, cancel := context.WithTimeout(ctx, time.Millisecond*30) 164 defer cancel() 165 err := ctrl.Run(ctx) 166 r.ErrorIs(err, context.DeadlineExceeded) 167 r.Len(client.deltas, 2) 168 assertDelta(t, client.deltas[1], castaipb.KubernetesDeltaItemEvent_DELTA_ADD, false) 169 }) 170 171 t.Run("add failed to send deltas back", func(t *testing.T) { 172 r := require.New(t) 173 client := newMockClient() 174 var receivedDeltasCount int 175 client.streamFunc = func() castaipb.RuntimeSecurityAgentAPI_KubernetesDeltaIngestClient { 176 return &mockStream{ 177 onSend: func(item *castaipb.KubernetesDeltaItem) error { 178 receivedDeltasCount++ 179 if receivedDeltasCount > 1 { 180 return errors.New("ups") 181 } 182 return nil 183 }, 184 } 185 } 186 187 ctrl := newTestController(log, client) 188 189 ctrl.OnAdd(dep1) 190 r.Len(ctrl.pendingItems, 1) 191 r.NoError(ctrl.process(ctx)) 192 r.Len(ctrl.pendingItems, 0) 193 194 ctrl.OnAdd(dep1) 195 r.Len(ctrl.pendingItems, 1) 196 r.NoError(ctrl.process(ctx)) 197 r.Len(ctrl.pendingItems, 1) 198 }) 199 200 t.Run("fail on initial delta send error", func(t *testing.T) { 201 r := require.New(t) 202 client := newMockClient() 203 client.streamFunc = func() castaipb.RuntimeSecurityAgentAPI_KubernetesDeltaIngestClient { 204 return &mockStream{ 205 onSend: func(item *castaipb.KubernetesDeltaItem) error { 206 return errors.New("ups") 207 }, 208 } 209 } 210 211 ctrl := newTestController(log, client) 212 213 ctrl.OnAdd(dep1) 214 r.Len(ctrl.pendingItems, 1) 215 r.ErrorIs(ctrl.process(ctx), context.DeadlineExceeded) 216 }) 217 218 t.Run("send multiple delta items", func(t *testing.T) { 219 dep1 := &appsv1.Deployment{ 220 TypeMeta: metav1.TypeMeta{Kind: "Deployment", APIVersion: "v1"}, 221 ObjectMeta: metav1.ObjectMeta{ 222 Name: "dep1", 223 Namespace: "default", 224 UID: types.UID("111b56a9-ab5e-4a35-93af-f092e2f63011"), 225 Labels: map[string]string{"l1": "v1"}, 226 }, 227 } 228 dep2 := &appsv1.Deployment{ 229 TypeMeta: metav1.TypeMeta{Kind: "Deployment", APIVersion: "v1"}, 230 ObjectMeta: metav1.ObjectMeta{ 231 Name: "dep2", 232 Namespace: "default", 233 UID: types.UID("111b56a9-ab5e-4a35-93af-f092e2f63012"), 234 Labels: map[string]string{"l2": "v2"}, 235 }, 236 } 237 238 r := require.New(t) 239 client := newMockClient() 240 ctrl := newTestController(log, client) 241 ctrl.castaiClient = client 242 ctrl.OnAdd(dep1) 243 ctrl.OnAdd(dep2) 244 245 err := ctrl.process(ctx) 246 r.NoError(err) 247 r.Len(client.deltas, 2) 248 sort.Slice(client.deltas, func(i, j int) bool { 249 return client.deltas[i].ObjectName < client.deltas[j].ObjectName 250 }) 251 r.Equal(dep1.Labels, client.deltas[0].ObjectLabels) 252 r.Equal(dep1.ObjectMeta.Name, client.deltas[0].ObjectName) 253 r.Equal(dep2.Labels, client.deltas[1].ObjectLabels) 254 r.Equal(dep2.ObjectMeta.Name, client.deltas[1].ObjectName) 255 }) 256 } 257 258 func newMockClient() *mockCastaiClient { 259 client := &mockCastaiClient{} 260 client.streamFunc = func() castaipb.RuntimeSecurityAgentAPI_KubernetesDeltaIngestClient { 261 return &mockStream{ 262 onSend: func(item *castaipb.KubernetesDeltaItem) error { 263 client.mu.Lock() 264 defer client.mu.Unlock() 265 client.deltas = append(client.deltas, item) 266 return nil 267 }, 268 } 269 } 270 return client 271 } 272 273 func newTestController(log *logging.Logger, client *mockCastaiClient) *Controller { 274 ctrl := NewController( 275 log, 276 Config{Interval: 1 * time.Millisecond, InitialDeltay: 1 * time.Millisecond, SendTimeout: 10 * time.Millisecond}, 277 client, 278 &mockPodOwnerGetter{}, 279 ) 280 ctrl.deltaSendMaxTries = 2 281 ctrl.deltaItemSendMaxTries = 2 282 return ctrl 283 } 284 285 type mockPodOwnerGetter struct { 286 } 287 288 func (m *mockPodOwnerGetter) GetOwnerUID(obj kube.Object) string { 289 return string(obj.GetUID()) 290 } 291 292 type mockCastaiClient struct { 293 deltas []*castaipb.KubernetesDeltaItem 294 mu sync.Mutex 295 streamFunc func() v1.RuntimeSecurityAgentAPI_KubernetesDeltaIngestClient 296 } 297 298 func (m *mockCastaiClient) KubernetesDeltaIngest(ctx context.Context, opts ...grpc.CallOption) (v1.RuntimeSecurityAgentAPI_KubernetesDeltaIngestClient, error) { 299 return m.streamFunc(), nil 300 } 301 302 type mockStream struct { 303 onSend func(item *castaipb.KubernetesDeltaItem) error 304 } 305 306 func (m *mockStream) Recv() (*castaipb.KubernetesDeltaIngestResponse, error) { 307 return &castaipb.KubernetesDeltaIngestResponse{}, nil 308 } 309 310 func (m *mockStream) Send(item *castaipb.KubernetesDeltaItem) error { 311 return m.onSend(item) 312 } 313 314 func (m *mockStream) CloseAndRecv() (*castaipb.KubernetesDeltaIngestResponse, error) { 315 return nil, nil 316 } 317 318 func (m *mockStream) Header() (metadata.MD, error) { 319 return nil, nil 320 } 321 322 func (m *mockStream) Trailer() metadata.MD { 323 return nil 324 } 325 326 func (m *mockStream) CloseSend() error { 327 return nil 328 329 } 330 331 func (m *mockStream) Context() context.Context { 332 return nil 333 334 } 335 336 func (m *mockStream) SendMsg(mmsg any) error { 337 return nil 338 339 } 340 341 func (m *mockStream) RecvMsg(mmsg any) error { 342 return nil 343 }