github.com/castai/kvisor@v1.7.1-0.20240516114728-b3572a2607b5/cmd/agent/daemon/state/controller_test.go (about) 1 package state 2 3 import ( 4 "context" 5 "net/netip" 6 "testing" 7 "time" 8 9 kubepb "github.com/castai/kvisor/api/v1/kube" 10 castaipb "github.com/castai/kvisor/api/v1/runtime" 11 "github.com/castai/kvisor/cmd/agent/daemon/enrichment" 12 "github.com/castai/kvisor/cmd/agent/daemon/netstats" 13 "github.com/castai/kvisor/pkg/cgroup" 14 "github.com/castai/kvisor/pkg/containers" 15 "github.com/castai/kvisor/pkg/ebpftracer" 16 "github.com/castai/kvisor/pkg/ebpftracer/types" 17 "github.com/castai/kvisor/pkg/logging" 18 "github.com/samber/lo" 19 "github.com/stretchr/testify/require" 20 "google.golang.org/grpc" 21 ) 22 23 func TestController(t *testing.T) { 24 ctx, cancel := context.WithCancel(context.Background()) 25 defer cancel() 26 27 t.Run("events pipeline", func(t *testing.T) { 28 r := require.New(t) 29 ctrl := newTestController() 30 exporter := &mockEventsExporter{events: make(chan *castaipb.Event, 10)} 31 ctrl.exporters.Events = append(ctrl.exporters.Events, exporter) 32 ctrl.tracer.(*mockEbpfTracer).eventsChan <- &types.Event{ 33 Context: &types.EventContext{Ts: 1}, 34 Container: &containers.Container{ 35 PodName: "p1", 36 }, 37 } 38 ctrlerr := make(chan error, 1) 39 go func() { 40 ctrlerr <- ctrl.Run(ctx) 41 }() 42 43 select { 44 case e := <-exporter.events: 45 r.Equal("p1", e.PodName) 46 case err := <-ctrlerr: 47 t.Fatal(err) 48 case <-time.After(time.Second): 49 t.Fatal("timed out waiting for data") 50 } 51 }) 52 53 t.Run("container stats pipeline", func(t *testing.T) { 54 r := require.New(t) 55 ctrl := newTestController() 56 exporter := &mockContainerStatsExporter{events: make(chan *castaipb.ContainerStatsBatch, 10)} 57 ctrl.exporters.ContainerStats = append(ctrl.exporters.ContainerStats, exporter) 58 ctrl.tracer.(*mockEbpfTracer).syscallStats = map[ebpftracer.SyscallStatsKeyCgroupID][]ebpftracer.SyscallStats{ 59 1: { 60 {ebpftracer.SyscallID(2), 3}, 61 }, 62 } 63 ctrl.containersClient.(*mockContainersClient).list = []*containers.Container{ 64 { 65 ID: "c1", 66 Name: "cont", 67 CgroupID: 1, 68 PodNamespace: "ns1", 69 PodUID: "p1", 70 PodName: "p1", 71 Cgroup: nil, 72 PIDs: []uint32{1}, 73 }, 74 } 75 76 ctrlerr := make(chan error, 1) 77 go func() { 78 ctrlerr <- ctrl.Run(ctx) 79 }() 80 81 select { 82 case e := <-exporter.events: 83 r.Len(e.Items, 1) 84 r.Len(e.Items[0].Stats, 1) 85 r.Equal(1, int(e.Items[0].Stats[0].Group)) 86 r.Equal(2, int(e.Items[0].Stats[0].Subgroup)) 87 r.GreaterOrEqual(1, int(e.Items[0].Stats[0].Value)) 88 case err := <-ctrlerr: 89 t.Fatal(err) 90 case <-time.After(time.Second): 91 t.Fatal("timed out waiting for data") 92 } 93 }) 94 95 t.Run("netflow pipeline", func(t *testing.T) { 96 r := require.New(t) 97 ctrl := newTestController() 98 exporter := &mockNetflowExporter{events: make(chan *castaipb.Netflow, 10)} 99 ctrl.exporters.Netflow = append(ctrl.exporters.Netflow, exporter) 100 101 ctrl.tracer.(*mockEbpfTracer).netflowEventsChan <- &types.Event{ 102 Context: &types.EventContext{Ts: 1}, 103 Container: &containers.Container{ 104 PodName: "p1", 105 }, 106 Args: types.NetFlowBaseArgs{ 107 Proto: 6, 108 Tuple: types.AddrTuple{ 109 Src: netip.MustParseAddrPort("10.10.0.10:34561"), 110 Dst: netip.MustParseAddrPort("10.10.0.15:80"), 111 }, 112 TxBytes: 10, 113 TxPackets: 5, 114 }, 115 } 116 117 ctrlerr := make(chan error, 1) 118 go func() { 119 ctrlerr <- ctrl.Run(ctx) 120 }() 121 122 select { 123 case e := <-exporter.events: 124 r.Equal(castaipb.NetflowProtocol_NETFLOW_PROTOCOL_TCP, e.Protocol) 125 r.Equal(netip.MustParseAddr("10.10.0.10").AsSlice(), e.Addr) 126 r.Equal(34561, int(e.Port)) 127 r.Len(e.Destinations, 1) 128 dest := e.Destinations[0] 129 r.Equal(netip.MustParseAddr("10.10.0.15").AsSlice(), dest.Addr) 130 r.Equal(80, int(dest.Port)) 131 r.Equal(10, int(dest.TxBytes)) 132 r.Equal(5, int(dest.TxPackets)) 133 case err := <-ctrlerr: 134 t.Fatal(err) 135 case <-time.After(time.Second): 136 t.Fatal("timed out waiting for data") 137 } 138 }) 139 140 t.Run("netflow cleanup", func(t *testing.T) { 141 r := require.New(t) 142 ctrl := newTestController() 143 144 ctrl.netflows = map[uint64]*netflowVal{ 145 1: { 146 updatedAt: time.Now(), 147 }, 148 2: { 149 updatedAt: time.Now().Add(-time.Hour), 150 }, 151 } 152 153 ctrl.cfg.NetflowExportInterval = 1 * time.Minute 154 ctrl.cleanupNetflow() 155 // Should keep recent flows. 156 r.Equal([]uint64{1}, lo.Keys(ctrl.netflows)) 157 }) 158 } 159 160 func newTestController() *Controller { 161 log := logging.NewTestLog() 162 cfg := Config{ 163 ContainerStatsScrapeInterval: time.Millisecond, 164 NetflowExportInterval: time.Millisecond, 165 NetflowCleanupInterval: 5 * time.Millisecond, 166 } 167 exporters := NewExporters(log) 168 contClient := &mockContainersClient{} 169 netReader := &mockNetStatsReader{} 170 ctClient := &mockConntrackClient{} 171 tracer := &mockEbpfTracer{eventsChan: make(chan *types.Event, 100), netflowEventsChan: make(chan *types.Event, 100)} 172 sigEngine := &mockSignatureEngine{eventsChan: make(chan *castaipb.Event, 100)} 173 enrichService := &mockEnrichmentService{eventsChan: make(chan *castaipb.Event, 100)} 174 kubeClient := &mockKubeClient{} 175 return NewController( 176 log, 177 cfg, 178 exporters, 179 contClient, 180 netReader, 181 ctClient, 182 tracer, 183 sigEngine, 184 enrichService, 185 kubeClient, 186 ) 187 } 188 189 type mockEventsExporter struct { 190 events chan *castaipb.Event 191 } 192 193 func (m *mockEventsExporter) Run(ctx context.Context) error { 194 return nil 195 } 196 197 func (m *mockEventsExporter) Enqueue(e *castaipb.Event) { 198 m.events <- e 199 } 200 201 type mockContainerStatsExporter struct { 202 events chan *castaipb.ContainerStatsBatch 203 } 204 205 func (m *mockContainerStatsExporter) Run(ctx context.Context) error { 206 return nil 207 } 208 209 func (m *mockContainerStatsExporter) Enqueue(e *castaipb.ContainerStatsBatch) { 210 m.events <- e 211 } 212 213 type mockNetflowExporter struct { 214 events chan *castaipb.Netflow 215 } 216 217 func (m *mockNetflowExporter) Run(ctx context.Context) error { 218 return nil 219 } 220 221 func (m *mockNetflowExporter) Enqueue(e *castaipb.Netflow) { 222 m.events <- e 223 } 224 225 type mockContainersClient struct { 226 list []*containers.Container 227 } 228 229 func (m *mockContainersClient) GetCgroupCpuStats(c *containers.Container) (*cgroup.CPUStat, error) { 230 return &cgroup.CPUStat{}, nil 231 } 232 233 func (m *mockContainersClient) GetCgroupMemoryStats(c *containers.Container) (*cgroup.MemoryStat, error) { 234 return &cgroup.MemoryStat{}, nil 235 } 236 237 func (m *mockContainersClient) ListContainers() []*containers.Container { 238 return m.list 239 } 240 241 func (m *mockContainersClient) GetContainerForCgroup(ctx context.Context, cgroup uint64) (*containers.Container, error) { 242 for _, v := range m.list { 243 if v.CgroupID == cgroup { 244 return v, nil 245 } 246 } 247 return nil, containers.ErrContainerNotFound 248 } 249 250 func (m *mockContainersClient) LookupContainerForCgroupInCache(cgroup uint64) (*containers.Container, bool, error) { 251 for _, v := range m.list { 252 if v.CgroupID == cgroup { 253 return v, true, nil 254 } 255 } 256 return nil, false, containers.ErrContainerNotFound 257 } 258 259 func (m *mockContainersClient) CleanupCgroup(cgroup cgroup.ID) { 260 return 261 } 262 263 func (m *mockContainersClient) GetCgroupsInNamespace(namespace string) []uint64 { 264 return []uint64{} 265 } 266 267 func (m *mockContainersClient) RegisterContainerCreatedListener(l containers.ContainerCreatedListener) { 268 return 269 } 270 271 func (m *mockContainersClient) RegisterContainerDeletedListener(l containers.ContainerDeletedListener) { 272 return 273 } 274 275 type mockNetStatsReader struct { 276 } 277 278 func (m *mockNetStatsReader) Read(pid uint32) ([]netstats.InterfaceStats, error) { 279 return nil, nil 280 } 281 282 type mockConntrackClient struct { 283 } 284 285 func (m *mockConntrackClient) GetDestination(src, dst netip.AddrPort) (netip.AddrPort, bool) { 286 return netip.AddrPort{}, false 287 } 288 289 type mockEbpfTracer struct { 290 eventsChan chan *types.Event 291 netflowEventsChan chan *types.Event 292 syscallStats map[ebpftracer.SyscallStatsKeyCgroupID][]ebpftracer.SyscallStats 293 } 294 295 func (m *mockEbpfTracer) ReadSyscallStats() (map[ebpftracer.SyscallStatsKeyCgroupID][]ebpftracer.SyscallStats, error) { 296 // Inc stats each time scrape is called since we export only stats which changed. 297 for _, s := range m.syscallStats { 298 for i := range s { 299 s[i].Count++ 300 } 301 } 302 return m.syscallStats, nil 303 } 304 305 func (m *mockEbpfTracer) Events() <-chan *types.Event { 306 return m.eventsChan 307 } 308 309 func (m *mockEbpfTracer) NetflowEvents() <-chan *types.Event { 310 return m.netflowEventsChan 311 } 312 313 func (m *mockEbpfTracer) MuteEventsFromCgroup(cgroup uint64) error { 314 return nil 315 } 316 317 func (m *mockEbpfTracer) MuteEventsFromCgroups(cgroups []uint64) error { 318 return nil 319 } 320 321 func (m *mockEbpfTracer) UnmuteEventsFromCgroup(cgroup uint64) error { 322 return nil 323 } 324 325 func (m *mockEbpfTracer) UnmuteEventsFromCgroups(cgroups []uint64) error { 326 return nil 327 } 328 329 func (m *mockEbpfTracer) IsCgroupMuted(cgroup uint64) bool { 330 return true 331 } 332 333 type mockSignatureEngine struct { 334 eventsChan chan *castaipb.Event 335 } 336 337 func (m *mockSignatureEngine) Events() <-chan *castaipb.Event { 338 return m.eventsChan 339 } 340 341 type mockEnrichmentService struct { 342 eventsChan chan *castaipb.Event 343 } 344 345 func (m *mockEnrichmentService) Events() <-chan *castaipb.Event { 346 return m.eventsChan 347 } 348 349 func (m *mockEnrichmentService) Enqueue(e *enrichment.EnrichRequest) bool { 350 return false 351 } 352 353 type mockKubeClient struct { 354 } 355 356 func (m *mockKubeClient) GetClusterInfo(ctx context.Context, in *kubepb.GetClusterInfoRequest, opts ...grpc.CallOption) (*kubepb.GetClusterInfoResponse, error) { 357 return &kubepb.GetClusterInfoResponse{ 358 PodsCidr: "10.0.0.0/16", 359 ServiceCidr: "172.168.0.0/16", 360 }, nil 361 } 362 363 func (m *mockKubeClient) GetIPInfo(ctx context.Context, in *kubepb.GetIPInfoRequest, opts ...grpc.CallOption) (*kubepb.GetIPInfoResponse, error) { 364 return &kubepb.GetIPInfoResponse{ 365 Info: &kubepb.IPInfo{}, 366 }, nil 367 } 368 369 func (m *mockKubeClient) GetPod(ctx context.Context, in *kubepb.GetPodRequest, opts ...grpc.CallOption) (*kubepb.GetPodResponse, error) { 370 return &kubepb.GetPodResponse{ 371 Pod: &kubepb.Pod{}, 372 }, nil 373 }