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  }