@@ -5,12 +5,23 @@ package integration
55
66import (
77 "bytes"
8+ "fmt"
89 "regexp"
10+ "slices"
911 "strconv"
1012 "strings"
1113 "testing"
1214
15+ "github.com/bpfman/bpfman-operator/apis/v1alpha1"
1316 "github.com/stretchr/testify/require"
17+ "k8s.io/apimachinery/pkg/api/meta"
18+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
19+ )
20+
21+ const (
22+ bpfmanNamespace = "bpfman"
23+ bpfmanContainer = "bpfman"
24+ bpfmanDaemonSelector = "name=bpfman-daemon"
1425)
1526
1627func doKprobeCheck (t * testing.T , output * bytes.Buffer ) bool {
@@ -131,3 +142,218 @@ func doProbeCommonCheck(t *testing.T, output *bytes.Buffer, str string) (bool, i
131142 }
132143 return false , 0
133144}
145+
146+ // clusterBpfApplicationStateSuccess returns a function that checks if the expected number of
147+ // ClusterBpfApplications matching the label selector have reached a successful state.
148+ func clusterBpfApplicationStateSuccess (t * testing.T , labelSelector string , numExpected int ) func () bool {
149+ return func () bool {
150+ // Fetch all ClusterBpfApplications matching the label selector.
151+ apps , err := bpfmanClient .BpfmanV1alpha1 ().ClusterBpfApplications ().List (ctx , metav1.ListOptions {
152+ LabelSelector : labelSelector ,
153+ })
154+ require .NoError (t , err )
155+
156+ // Count how many applications have reached success state.
157+ numMatches := 0
158+ for _ , app := range apps .Items {
159+ c := meta .FindStatusCondition (app .Status .Conditions , string (v1alpha1 .BpfAppStateCondSuccess ))
160+ if c != nil && c .Status == metav1 .ConditionTrue {
161+ numMatches ++
162+ }
163+ }
164+ // Return true if the number of successful applications matches expected count.
165+ return numMatches == numExpected
166+ }
167+ }
168+
169+ // verifyClusterBpfApplicationPriority returns a function that verifies BPF program links are ordered
170+ // correctly according to their priority values on each node.
171+ func verifyClusterBpfApplicationPriority (t * testing.T , labelSelector string ) func () bool {
172+ return func () bool {
173+ // Fetch all ClusterBpfApplications matching the label selector.
174+ apps , err := bpfmanClient .BpfmanV1alpha1 ().ClusterBpfApplications ().List (ctx , metav1.ListOptions {
175+ LabelSelector : labelSelector ,
176+ })
177+ require .NoError (t , err )
178+
179+ // Fetch all ClusterBpfApplicationStates to get per-node link information.
180+ appStates , err := bpfmanClient .BpfmanV1alpha1 ().ClusterBpfApplicationStates ().List (ctx , metav1.ListOptions {})
181+ require .NoError (t , err )
182+
183+ // Build a map of node names to their associated links from ClusterBpfApplicationStates.
184+ nodeLinks := map [string ][]link {}
185+ for _ , app := range apps .Items {
186+ for _ , appState := range appStates .Items {
187+ for _ , ownerRef := range appState .OwnerReferences {
188+ // Skip if this appState is not controlled by the current app.
189+ if ownerRef .Controller == nil || ! * ownerRef .Controller {
190+ continue
191+ }
192+ if ownerRef .UID != app .UID {
193+ continue
194+ }
195+ // Initialize the slice for this node if needed.
196+ if nodeLinks [appState .Status .Node ] == nil {
197+ nodeLinks [appState .Status .Node ] = []link {}
198+ }
199+ // Extract and append links from this appState.
200+ nodeLinks [appState .Status .Node ] = append (
201+ nodeLinks [appState .Status .Node ],
202+ getClusterBpfApplicationStateLinks (t , appState )... ,
203+ )
204+ }
205+ }
206+ }
207+ // Verify link ordering on each node by directly querying bpfman daemon inside the pod.
208+ for node , appStateLinks := range nodeLinks {
209+ bpfmanLinks := []link {}
210+ // Find the bpfman daemon pod running on this node.
211+ pods , err := env .Cluster ().Client ().CoreV1 ().Pods (bpfmanNamespace ).List (ctx , metav1.ListOptions {
212+ LabelSelector : bpfmanDaemonSelector ,
213+ FieldSelector : fmt .Sprintf ("spec.nodeName=%s" , node ),
214+ })
215+ require .NoError (t , err )
216+ require .Len (t , pods .Items , 1 )
217+ // Query each link from bpfman and verify that bpfman get link matches the output from
218+ // ClusterBpfApplicationState.
219+ for _ , appStateLink := range appStateLinks {
220+ cmd := []string {"./bpfman" , "get" , "link" , fmt .Sprintf ("%d" , appStateLink .linkId )}
221+ var bpfmanOut , bpfmanErr bytes.Buffer
222+ err := podExec (ctx , t , pods .Items [0 ], bpfmanContainer , & bpfmanOut , & bpfmanErr , cmd )
223+ require .NoError (t , err )
224+ t .Logf ("bpfman get link output:\n %s" , bpfmanOut .String ())
225+ // Parse the bpfman output and verify it matches.
226+ bpfmanLink := parseLink (bpfmanOut .String ())
227+ require .True (t , linkOutputMatchesLink (t , bpfmanLink , appStateLink ))
228+ bpfmanLinks = append (bpfmanLinks , bpfmanLink )
229+ }
230+ // Verify that links are ordered correctly by priority (match priority to expected position).
231+ require .True (t , verifyLinkOrder (bpfmanLinks ), "position in slice should match priority" , bpfmanLinks )
232+ }
233+ return true
234+ }
235+ }
236+
237+ // link represents a BPF program link with its metadata including link ID, network interface,
238+ // namespace path, priority, and position in the link chain.
239+ type link struct {
240+ linkId uint32
241+ interfaceName string
242+ netnsPath string
243+ priority int32
244+ position int32
245+ }
246+
247+ // parseLink parses the output from "bpfman get link" command and converts it to a link struct.
248+ func parseLink (out string ) link {
249+ l := link {}
250+ lines := bytes .Split ([]byte (out ), []byte ("\n " ))
251+
252+ for _ , line := range lines {
253+ parts := bytes .SplitN (line , []byte (":" ), 2 )
254+ if len (parts ) != 2 {
255+ continue
256+ }
257+ key := bytes .TrimSpace (parts [0 ])
258+ value := bytes .TrimSpace (parts [1 ])
259+
260+ switch string (key ) {
261+ case "Link ID" :
262+ fmt .Sscanf (string (value ), "%d" , & l .linkId )
263+ case "Interface" :
264+ l .interfaceName = string (value )
265+ case "Network Namespace" :
266+ if string (value ) != "None" {
267+ l .netnsPath = string (value )
268+ }
269+ case "Priority" :
270+ fmt .Sscanf (string (value ), "%d" , & l .priority )
271+ case "Position" :
272+ fmt .Sscanf (string (value ), "%d" , & l .position )
273+ }
274+ }
275+
276+ return l
277+ }
278+
279+ // getClusterBpfApplicationStateLinks extracts link information from a ClusterBpfApplicationState
280+ // for XDP, TC, and TCX program types.
281+ func getClusterBpfApplicationStateLinks (t * testing.T , appState v1alpha1.ClusterBpfApplicationState ) []link {
282+ links := []link {}
283+ // Iterate through all programs in the application state.
284+ for _ , program := range appState .Status .Programs {
285+ switch program .Type {
286+ case v1alpha1 .ProgTypeXDP :
287+ // Extract XDP program links.
288+ for _ , l := range program .XDP .Links {
289+ require .NotNil (t , l .LinkId )
290+ require .NotNil (t , l .Priority )
291+ links = append (links , link {
292+ linkId : * l .LinkId ,
293+ interfaceName : l .InterfaceName ,
294+ netnsPath : l .NetnsPath ,
295+ priority : * l .Priority ,
296+ })
297+ }
298+ case v1alpha1 .ProgTypeTC :
299+ // Extract TC program links.
300+ for _ , l := range program .TC .Links {
301+ require .NotNil (t , l .LinkId )
302+ require .NotNil (t , l .Priority )
303+ links = append (links , link {
304+ linkId : * l .LinkId ,
305+ interfaceName : l .InterfaceName ,
306+ netnsPath : l .NetnsPath ,
307+ priority : * l .Priority ,
308+ })
309+ }
310+ case v1alpha1 .ProgTypeTCX :
311+ // Extract TCX program links.
312+ for _ , l := range program .TCX .Links {
313+ require .NotNil (t , l .LinkId )
314+ require .NotNil (t , l .Priority )
315+ links = append (links , link {
316+ linkId : * l .LinkId ,
317+ interfaceName : l .InterfaceName ,
318+ netnsPath : l .NetnsPath ,
319+ priority : * l .Priority ,
320+ })
321+ }
322+ }
323+ }
324+ return links
325+ }
326+
327+ // linkOutputMatchesLink compares a link parsed from bpfman output with an expected link state.
328+ func linkOutputMatchesLink (t * testing.T , linkFromOutput , l link ) bool {
329+ t .Logf ("Comparing output and desired link state; got:\n %+v\n wanted:\n %+v" , linkFromOutput , l )
330+ return l .linkId == linkFromOutput .linkId &&
331+ l .interfaceName == linkFromOutput .interfaceName &&
332+ l .netnsPath == linkFromOutput .netnsPath &&
333+ l .priority == linkFromOutput .priority
334+ }
335+
336+ // verifyLinkOrder verifies that links' positions match their priorities.
337+ // Side-effect: this orders `links` in place by position.
338+ func verifyLinkOrder (links []link ) bool {
339+ // Order elements by position.
340+ slices .SortFunc (links , func (a , b link ) int {
341+ if a .position < b .position {
342+ return - 1
343+ }
344+ if a .position > b .position {
345+ return 1
346+ }
347+ return 0
348+ })
349+
350+ // Now, make sure that the priority of each element is >= the preceding element.
351+ oldI := 0
352+ for i := 1 ; i < len (links ); i ++ {
353+ if links [i ].priority < links [oldI ].priority {
354+ return false
355+ }
356+ oldI = i
357+ }
358+ return true
359+ }
0 commit comments