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  }