github.com/castai/kvisor@v1.7.1-0.20240516114728-b3572a2607b5/cmd/controller/state/kubebench/controller_test.go (about) 1 package kubebench 2 3 import ( 4 "bytes" 5 "context" 6 "io" 7 "os" 8 "reflect" 9 "testing" 10 "time" 11 12 castaipb "github.com/castai/kvisor/api/v1/runtime" 13 "k8s.io/client-go/kubernetes" 14 15 "github.com/castai/kvisor/cmd/controller/kube" 16 "github.com/castai/kvisor/pkg/logging" 17 "github.com/google/uuid" 18 "github.com/stretchr/testify/require" 19 "google.golang.org/grpc" 20 corev1 "k8s.io/api/core/v1" 21 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 "k8s.io/apimachinery/pkg/types" 23 "k8s.io/client-go/kubernetes/fake" 24 ) 25 26 var castaiNamespace = "castai-sec" 27 28 func TestSubscriber(t *testing.T) { 29 t.Run("creates job and sends report from log reader", func(t *testing.T) { 30 r := require.New(t) 31 ctx := context.Background() 32 clientset := fake.NewSimpleClientset() 33 mockCast := &mockCastaiClient{} 34 log := logging.NewTestLog() 35 logProvider := newMockLogProvider(readReport()) 36 kubeCtrl := &mockKubeController{} 37 38 ctrl := newTestController(log, clientset, mockCast, logProvider, kubeCtrl) 39 ctrl.finishedJobDeleteWaitDuration = 0 40 41 jobName := generateName("test_node") 42 43 // fake clientset doesn't create pod for job 44 _, err := clientset.CoreV1().Pods(castaiNamespace).Create(ctx, 45 &corev1.Pod{ 46 ObjectMeta: metav1.ObjectMeta{ 47 Labels: map[string]string{ 48 labelJobName: jobName, 49 }, 50 Namespace: castaiNamespace, 51 }, 52 Status: corev1.PodStatus{ 53 Phase: corev1.PodSucceeded, 54 }, 55 }, metav1.CreateOptions{}) 56 r.NoError(err) 57 58 node := &corev1.Node{ 59 TypeMeta: metav1.TypeMeta{ 60 Kind: "Node", 61 APIVersion: "v1", 62 }, 63 ObjectMeta: metav1.ObjectMeta{ 64 Name: "test_node", 65 UID: types.UID(uuid.NewString()), 66 }, 67 Status: corev1.NodeStatus{ 68 Conditions: []corev1.NodeCondition{ 69 { 70 Type: corev1.NodeReady, 71 Status: corev1.ConditionTrue, 72 }, 73 }, 74 }, 75 } 76 ctrl.OnAdd(node) 77 78 ctx, cancel := context.WithTimeout(ctx, 1000*time.Millisecond) 79 defer cancel() 80 err = ctrl.Run(ctx) 81 r.ErrorIs(err, context.DeadlineExceeded) 82 // Job should be deleted. 83 _, err = clientset.BatchV1().Jobs(castaiNamespace).Get(ctx, jobName, metav1.GetOptions{}) 84 r.Error(err) 85 r.Equal([]reflect.Type{reflect.TypeOf(&corev1.Node{})}, ctrl.RequiredInformers()) 86 87 r.Len(mockCast.reports, 1) 88 }) 89 90 t.Run("skip already scanned node", func(t *testing.T) { 91 r := require.New(t) 92 ctx := context.Background() 93 clientset := fake.NewSimpleClientset() 94 mockCast := &mockCastaiClient{} 95 log := logging.NewTestLog() 96 logProvider := newMockLogProvider(readReport()) 97 kubeCtrl := &mockKubeController{} 98 99 ctrl := newTestController(log, clientset, mockCast, logProvider, kubeCtrl) 100 nodeID := types.UID(uuid.NewString()) 101 ctrl.scannedNodes.Add(string(nodeID), struct{}{}) 102 103 node := &corev1.Node{ 104 TypeMeta: metav1.TypeMeta{ 105 Kind: "Node", 106 APIVersion: "v1", 107 }, 108 ObjectMeta: metav1.ObjectMeta{ 109 Name: "test_node", 110 UID: nodeID, 111 }, 112 Status: corev1.NodeStatus{ 113 Conditions: []corev1.NodeCondition{ 114 { 115 Type: corev1.NodeReady, 116 Status: corev1.ConditionTrue, 117 }, 118 }, 119 }, 120 } 121 ctrl.OnAdd(node) 122 ctrl.OnUpdate(node) 123 124 ctx, cancel := context.WithTimeout(ctx, 1000*time.Millisecond) 125 defer cancel() 126 err := ctrl.Run(ctx) 127 r.ErrorIs(err, context.DeadlineExceeded) 128 }) 129 130 t.Run("use cached report", func(t *testing.T) { 131 r := require.New(t) 132 ctx := context.Background() 133 clientset := fake.NewSimpleClientset() 134 mockCast := &mockCastaiClient{} 135 136 log := logging.NewTestLog() 137 logProvider := newMockLogProvider(readReport()) 138 kubeCtrl := &mockKubeController{} 139 140 ctrl := newTestController(log, clientset, mockCast, logProvider, kubeCtrl) 141 nodeID := types.UID(uuid.NewString()) 142 node := &corev1.Node{ 143 TypeMeta: metav1.TypeMeta{ 144 Kind: "Node", 145 APIVersion: "v1", 146 }, 147 ObjectMeta: metav1.ObjectMeta{ 148 Name: "test_node", 149 UID: nodeID, 150 }, 151 Status: corev1.NodeStatus{ 152 Conditions: []corev1.NodeCondition{ 153 { 154 Type: corev1.NodeReady, 155 Status: corev1.ConditionTrue, 156 }, 157 }, 158 }, 159 } 160 nodeGroupKey := getNodeGroupKey(node) 161 ctrl.kubeBenchReportsCache = map[uint64]*castaipb.KubeBenchReport{ 162 nodeGroupKey: {}, 163 } 164 ctrl.OnAdd(node) 165 ctrl.OnUpdate(node) 166 167 ctx, cancel := context.WithTimeout(ctx, 1000*time.Millisecond) 168 defer cancel() 169 err := ctrl.Run(ctx) 170 r.ErrorIs(err, context.DeadlineExceeded) 171 r.Len(mockCast.reports, 1) 172 }) 173 } 174 175 func TestNodeGroupKey(t *testing.T) { 176 r := require.New(t) 177 n1 := &corev1.Node{ 178 ObjectMeta: metav1.ObjectMeta{ 179 Labels: map[string]string{ 180 "provisioner.cast.ai/node-configuration-name": "default", 181 "provisioner.cast.ai/node-configuration-version": "v1", 182 }, 183 }, 184 Status: corev1.NodeStatus{ 185 NodeInfo: corev1.NodeSystemInfo{ 186 KernelVersion: "k1", 187 OSImage: "os1", 188 ContainerRuntimeVersion: "containerd", 189 KubeletVersion: "kubelet 1.1.1", 190 Architecture: "amd64", 191 }, 192 }, 193 } 194 n2 := &corev1.Node{ 195 Status: corev1.NodeStatus{ 196 NodeInfo: corev1.NodeSystemInfo{ 197 KernelVersion: "k2", 198 OSImage: "os2", 199 ContainerRuntimeVersion: "containerd", 200 KubeletVersion: "kubelet 2.2.2", 201 Architecture: "amd64", 202 }, 203 }, 204 } 205 206 key1 := getNodeGroupKey(n1) 207 r.Equal(key1, getNodeGroupKey(n1)) 208 key2 := getNodeGroupKey(n2) 209 r.NotEqual(key1, key2) 210 } 211 212 func newTestController(log *logging.Logger, clientset kubernetes.Interface, mockCast castaiClient, logProvider kube.PodLogProvider, kubeCtrl kubeController) *Controller { 213 ctrl := NewController( 214 log, 215 clientset, 216 Config{ 217 ScanInterval: 5 * time.Millisecond, 218 CloudProvider: "gke", 219 JobNamespace: castaiNamespace, 220 }, 221 mockCast, 222 logProvider, 223 kubeCtrl, 224 nil, 225 ) 226 return ctrl 227 } 228 229 func newMockLogProvider(b []byte) kube.PodLogProvider { 230 return &mockProvider{logs: b} 231 } 232 233 type mockProvider struct { 234 logs []byte 235 } 236 237 func (m *mockProvider) GetLogReader(_ context.Context, _, _ string) (io.ReadCloser, error) { 238 return io.NopCloser(bytes.NewReader(m.logs)), nil 239 } 240 241 func readReport() []byte { 242 file, _ := os.OpenFile("./testdata/kube-bench-gke.json", os.O_RDONLY, 0666) 243 reportBytes, _ := io.ReadAll(file) 244 245 return reportBytes 246 } 247 248 type mockKubeController struct { 249 } 250 251 func (m *mockKubeController) GetKvisorAgentImageDetails() (kube.ImageDetails, bool) { 252 return kube.ImageDetails{ 253 ScannerImageName: "kvisor-scanners", 254 ImagePullSecrets: nil, 255 }, true 256 } 257 258 func (m *mockKubeController) GetPodOwnerID(pod *corev1.Pod) string { 259 return string(pod.UID) 260 } 261 262 type mockCastaiClient struct { 263 reports []*castaipb.KubeBenchReport 264 } 265 266 func (m *mockCastaiClient) KubeBenchReportIngest(ctx context.Context, in *castaipb.KubeBenchReport, opts ...grpc.CallOption) (*castaipb.KubeBenchReportIngestResponse, error) { 267 m.reports = append(m.reports, in) 268 return &castaipb.KubeBenchReportIngestResponse{}, nil 269 }