github.com/cilium/cilium@v1.16.2/pkg/bpf/ops_linux_test.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package bpf
     5  
     6  import (
     7  	"context"
     8  	"encoding"
     9  	"errors"
    10  	"testing"
    11  
    12  	"github.com/cilium/ebpf"
    13  	"github.com/cilium/hive/cell"
    14  	"github.com/cilium/hive/hivetest"
    15  	"github.com/cilium/statedb"
    16  	"github.com/cilium/statedb/index"
    17  	"github.com/cilium/statedb/reconciler"
    18  	"github.com/sirupsen/logrus"
    19  	"github.com/stretchr/testify/assert"
    20  	"github.com/stretchr/testify/require"
    21  
    22  	"github.com/cilium/cilium/pkg/hive"
    23  	"github.com/cilium/cilium/pkg/logging"
    24  	"github.com/cilium/cilium/pkg/testutils"
    25  	"github.com/cilium/cilium/pkg/time"
    26  )
    27  
    28  type TestObject struct {
    29  	Key    TestKey
    30  	Value  TestValue
    31  	Status reconciler.Status
    32  }
    33  
    34  func (o *TestObject) BinaryKey() encoding.BinaryMarshaler {
    35  	return StructBinaryMarshaler{&o.Key}
    36  }
    37  
    38  func (o *TestObject) BinaryValue() encoding.BinaryMarshaler {
    39  	return StructBinaryMarshaler{&o.Value}
    40  }
    41  
    42  type emptyIterator struct{}
    43  
    44  func (*emptyIterator) Next() (*TestObject, uint64, bool) {
    45  	return nil, 0, false
    46  }
    47  
    48  var _ statedb.Iterator[*TestObject] = &emptyIterator{}
    49  
    50  func Test_MapOps(t *testing.T) {
    51  	testutils.PrivilegedTest(t)
    52  
    53  	testMap := NewMap("cilium_ops_test",
    54  		ebpf.Hash,
    55  		&TestKey{},
    56  		&TestValue{},
    57  		maxEntries,
    58  		BPF_F_NO_PREALLOC,
    59  	)
    60  
    61  	err := testMap.OpenOrCreate()
    62  	require.NoError(t, err, "OpenOrCreate")
    63  	defer testMap.Close()
    64  
    65  	ctx := context.TODO()
    66  	ops := NewMapOps[*TestObject](testMap)
    67  	obj := &TestObject{Key: TestKey{1}, Value: TestValue{2}}
    68  
    69  	// Test Update() and Delete()
    70  	err = ops.Update(ctx, nil, obj)
    71  	assert.NoError(t, err, "Update")
    72  
    73  	err = ops.Update(ctx, nil, obj)
    74  	assert.NoError(t, err, "Update")
    75  
    76  	v, err := testMap.Lookup(&TestKey{1})
    77  	assert.NoError(t, err, "Lookup")
    78  	assert.Equal(t, v.(*TestValue).Value, obj.Value.Value)
    79  
    80  	err = ops.Delete(ctx, nil, obj)
    81  	assert.NoError(t, err, "Delete")
    82  
    83  	_, err = testMap.Lookup(&TestKey{1})
    84  	assert.Error(t, err, "Lookup")
    85  
    86  	// Test Prune()
    87  	err = testMap.Update(&TestKey{2}, &TestValue{3})
    88  	assert.NoError(t, err, "Update")
    89  
    90  	v, err = testMap.Lookup(&TestKey{2})
    91  	if assert.NoError(t, err, "Lookup") {
    92  		assert.Equal(t, v.(*TestValue).Value, uint32(3))
    93  	}
    94  
    95  	// Give Prune() an empty set of objects, which should cause it to
    96  	// remove everything.
    97  	err = ops.Prune(ctx, nil, &emptyIterator{})
    98  	assert.NoError(t, err, "Prune")
    99  
   100  	data := map[string][]string{}
   101  	testMap.Dump(data)
   102  	assert.Len(t, data, 0)
   103  }
   104  
   105  func Test_MapOpsPrune(t *testing.T) {
   106  	testutils.PrivilegedTest(t)
   107  
   108  	// This tests pruning with an LPM trie. This ensures we do not regress, as
   109  	// previously we had issues with Prune concurrently iterating and deleting
   110  	// entries, which caused the iteration to skip entries
   111  	testMap := NewMap(
   112  		"cilium_ops_prune_test",
   113  		ebpf.LPMTrie,
   114  		&TestLPMKey{},
   115  		&TestValue{},
   116  		maxEntries,
   117  		BPF_F_NO_PREALLOC,
   118  	)
   119  	err := testMap.OpenOrCreate()
   120  	require.NoError(t, err, "OpenOrCreate")
   121  	defer testMap.Close()
   122  
   123  	ctx := context.TODO()
   124  	ops := NewMapOps[*TestObject](testMap)
   125  
   126  	// Fill map with similarly prefixed entries
   127  	err = testMap.Update(&TestLPMKey{32, 0xFF00_00FF}, &TestValue{0})
   128  	assert.NoError(t, err, "Update 0")
   129  	err = testMap.Update(&TestLPMKey{32, 0xFF01_01FF}, &TestValue{1})
   130  	assert.NoError(t, err, "Update 1")
   131  	err = testMap.Update(&TestLPMKey{32, 0xFF02_02FF}, &TestValue{2})
   132  	assert.NoError(t, err, "Update 2")
   133  	err = testMap.Update(&TestLPMKey{32, 0xFF03_03FF}, &TestValue{3})
   134  	assert.NoError(t, err, "Update 3")
   135  
   136  	// Prune should now remove everything
   137  	err = ops.Prune(ctx, nil, &emptyIterator{})
   138  	assert.NoError(t, err, "Prune")
   139  
   140  	data := map[string][]string{}
   141  	testMap.Dump(data)
   142  	assert.Len(t, data, 0)
   143  }
   144  
   145  // Test_MapOps_ReconcilerExample serves as a testable example for the map ops.
   146  // This is not an "Example*" function as it can only run privileged.
   147  func Test_MapOps_ReconcilerExample(t *testing.T) {
   148  	testutils.PrivilegedTest(t)
   149  
   150  	exampleMap := NewMap("example",
   151  		ebpf.Hash,
   152  		&TestKey{},
   153  		&TestValue{},
   154  		maxEntries,
   155  		BPF_F_NO_PREALLOC,
   156  	)
   157  	err := exampleMap.OpenOrCreate()
   158  	require.NoError(t, err)
   159  	t.Cleanup(func() { exampleMap.Close() })
   160  
   161  	// Create the table containing the desired state of the map.
   162  	keyIndex := statedb.Index[*TestObject, uint32]{
   163  		Name: "example",
   164  		FromObject: func(obj *TestObject) index.KeySet {
   165  			return index.NewKeySet(index.Uint32(obj.Key.Key))
   166  		},
   167  		FromKey: index.Uint32,
   168  		Unique:  true,
   169  	}
   170  	table, err := statedb.NewTable("example", keyIndex)
   171  	require.NoError(t, err, "NewTable")
   172  
   173  	// Create the map operations and the reconciler configuration.
   174  	ops := NewMapOps[*TestObject](exampleMap)
   175  
   176  	// Silence the hive log output.
   177  	oldLogLevel := logging.DefaultLogger.GetLevel()
   178  	logging.SetLogLevel(logrus.ErrorLevel)
   179  	t.Cleanup(func() {
   180  		logging.SetLogLevel(oldLogLevel)
   181  	})
   182  
   183  	// Setup and start a hive to run the reconciler.
   184  	var db *statedb.DB
   185  	h := hive.New(
   186  		cell.Module(
   187  			"example",
   188  			"Example",
   189  
   190  			cell.Invoke(
   191  				func(db_ *statedb.DB) error {
   192  					db = db_
   193  					return db.RegisterTable(table)
   194  				},
   195  			),
   196  			cell.Invoke(
   197  				func(params reconciler.Params) error {
   198  					_, err := reconciler.Register[*TestObject](
   199  						params,
   200  						table,
   201  						func(obj *TestObject) *TestObject {
   202  							obj2 := *obj
   203  							return &obj2
   204  						},
   205  						func(obj *TestObject, s reconciler.Status) *TestObject {
   206  							obj.Status = s
   207  							return obj
   208  						},
   209  						func(obj *TestObject) reconciler.Status {
   210  							return obj.Status
   211  						},
   212  						ops,
   213  						nil,
   214  					)
   215  					return err
   216  				}),
   217  		),
   218  	)
   219  
   220  	tlog := hivetest.Logger(t)
   221  	err = h.Start(tlog, context.Background())
   222  	require.NoError(t, err, "Start")
   223  
   224  	t.Cleanup(func() {
   225  		h.Stop(tlog, context.Background())
   226  	})
   227  
   228  	// Insert an object to the desired state and wait for it to reconcile.
   229  	txn := db.WriteTxn(table)
   230  	table.Insert(txn, &TestObject{
   231  		Key:   TestKey{1},
   232  		Value: TestValue{2},
   233  
   234  		// Mark the object to be pending for reconciliation. Without this
   235  		// the reconciler would ignore this object.
   236  		Status: reconciler.StatusPending(),
   237  	})
   238  	txn.Commit()
   239  
   240  	for {
   241  		obj, _, watch, ok := table.GetWatch(db.ReadTxn(), keyIndex.Query(1))
   242  		if ok {
   243  			if obj.Status.Kind == reconciler.StatusKindDone {
   244  				// The object has been reconciled.
   245  				break
   246  			}
   247  			t.Logf("Object not done yet: %#v", obj)
   248  		}
   249  		// Wait for the object to update
   250  		<-watch
   251  	}
   252  
   253  	v, err := exampleMap.Lookup(&TestKey{1})
   254  	require.NoError(t, err, "Lookup")
   255  	require.Equal(t, uint32(2), v.(*TestValue).Value)
   256  
   257  	// Mark the object for deletion
   258  	txn = db.WriteTxn(table)
   259  	table.Delete(txn, &TestObject{
   260  		Key:   TestKey{1},
   261  		Value: TestValue{2},
   262  	})
   263  	txn.Commit()
   264  
   265  	for {
   266  		obj, _, watch, ok := table.GetWatch(db.ReadTxn(), keyIndex.Query(1))
   267  		if !ok {
   268  			// The object has been successfully deleted.
   269  			break
   270  		}
   271  		t.Logf("Object not deleted yet: %#v", obj)
   272  		// Wait for the object to update
   273  		<-watch
   274  	}
   275  
   276  	require.Eventually(
   277  		t,
   278  		func() bool {
   279  			_, err = exampleMap.Lookup(&TestKey{1})
   280  			return errors.Is(err, ebpf.ErrKeyNotExist)
   281  		},
   282  		time.Second,
   283  		100*time.Millisecond,
   284  		"Expected key to eventually be removed")
   285  
   286  }