Skip to content

Commit bbd6448

Browse files
committed
test-integration: Add priority ordering verification for BPF links
Add integration tests to verify BPF program links are correctly ordered by priority across XDP, TC, and TCX program types. The new verification framework validates link ordering on each cluster node by comparing ClusterBpfApplicationState data against actual bpfman daemon state. Signed-off-by: Andreas Karis <[email protected]>
1 parent 11bcd1f commit bbd6448

File tree

5 files changed

+463
-11
lines changed

5 files changed

+463
-11
lines changed

test/integration/common.go

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,23 @@ package integration
55

66
import (
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

1627
func 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\nwanted:\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+
}

test/integration/metrics_test.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ func testMetricsProxySelfTest(ctx context.Context, t *testing.T, pod corev1.Pod,
179179
cmd := []string{"env", "TOKEN=" + token, "/metrics-proxy", "test"}
180180

181181
var stdout, stderr bytes.Buffer
182-
err := podExec(ctx, t, pod, &stdout, &stderr, cmd)
182+
err := podExec(ctx, t, pod, "", &stdout, &stderr, cmd)
183183

184184
// For self-test, we expect exit code 0 for success, non-zero
185185
// for failure. Parse the JSON regardless of exit code to get
@@ -244,7 +244,8 @@ func testMetricsProxySelfTest(ctx context.Context, t *testing.T, pod corev1.Pod,
244244
t.Logf("All self-tests passed successfully on pod %s", pod.Name)
245245
}
246246

247-
func podExec(ctx context.Context, t *testing.T, pod corev1.Pod, stdout, stderr *bytes.Buffer, cmd []string) error {
247+
// podExec executes a command in a pod's container and captures stdout/stderr output.
248+
func podExec(ctx context.Context, t *testing.T, pod corev1.Pod, container string, stdout, stderr *bytes.Buffer, cmd []string) error {
248249
t.Helper()
249250
kubeConfig, err := config.GetConfig()
250251
if err != nil {
@@ -269,6 +270,9 @@ func podExec(ctx context.Context, t *testing.T, pod corev1.Pod, stdout, stderr *
269270
Stderr: true,
270271
TTY: false,
271272
}
273+
if container != "" {
274+
execOptions.Container = container
275+
}
272276

273277
req.VersionedParams(execOptions, scheme.ParameterCodec)
274278

test/integration/tc_test.go

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ package integration
55

66
import (
77
"bytes"
8-
"context"
8+
"fmt"
99
"io"
1010
"testing"
1111
"time"
@@ -14,20 +14,23 @@ import (
1414
"github.com/stretchr/testify/require"
1515
corev1 "k8s.io/api/core/v1"
1616
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
17+
"k8s.io/utils/ptr"
1718
)
1819

1920
const (
2021
tcGoCounterKustomize = "https://github.com/bpfman/bpfman/examples/config/default/go-tc-counter/?timeout=120&ref=main"
2122
tcGoCounterUserspaceNs = "go-tc-counter"
2223
tcGoCounterUserspaceDsName = "go-tc-counter-ds"
24+
tcGoCounterBytecodeName = "go-tc-counter-example"
25+
tcByteCodeLabelSelector = "app.kubernetes.io/name=tcprogram"
2326
)
2427

2528
func TestTcGoCounter(t *testing.T) {
2629
t.Log("deploying tc counter program")
2730
require.NoError(t, clusters.KustomizeDeployForCluster(ctx, env.Cluster(), tcGoCounterKustomize))
28-
addCleanup(func(context.Context) error {
31+
t.Cleanup(func() {
2932
cleanupLog("cleaning up tc counter program")
30-
return clusters.KustomizeDeleteForCluster(ctx, env.Cluster(), tcGoCounterKustomize)
33+
clusters.KustomizeDeleteForCluster(ctx, env.Cluster(), tcGoCounterKustomize)
3134
})
3235

3336
t.Log("waiting for go tc counter userspace daemon to be available")
@@ -41,6 +44,7 @@ func TestTcGoCounter(t *testing.T) {
4144

4245
pods, err := env.Cluster().Client().CoreV1().Pods(tcGoCounterUserspaceNs).List(ctx, metav1.ListOptions{LabelSelector: "name=go-tc-counter"})
4346
require.NoError(t, err)
47+
require.Len(t, pods.Items, 1)
4448
gotcCounterPod := pods.Items[0]
4549

4650
req := env.Cluster().Client().CoreV1().Pods(tcGoCounterUserspaceNs).GetLogs(gotcCounterPod.Name, &corev1.PodLogOptions{})
@@ -57,3 +61,73 @@ func TestTcGoCounter(t *testing.T) {
5761
return doTcCheck(t, output)
5862
}, 30*time.Second, time.Second)
5963
}
64+
65+
func TestTcGoCounterLinkPriority(t *testing.T) {
66+
priorities := []*int32{
67+
nil,
68+
ptr.To(int32(0)),
69+
ptr.To(int32(500)),
70+
ptr.To(int32(1000)),
71+
}
72+
73+
t.Log("deploying tc counter program")
74+
require.NoError(t, clusters.KustomizeDeployForCluster(ctx, env.Cluster(), tcGoCounterKustomize))
75+
t.Cleanup(func() {
76+
cleanupLog("cleaning up tc counter program")
77+
clusters.KustomizeDeleteForCluster(ctx, env.Cluster(), tcGoCounterKustomize)
78+
79+
cleanupLog("cleaning up tc counter bytecode")
80+
bpfmanClient.BpfmanV1alpha1().ClusterBpfApplications().DeleteCollection(ctx, metav1.DeleteOptions{},
81+
metav1.ListOptions{
82+
LabelSelector: tcByteCodeLabelSelector,
83+
})
84+
})
85+
86+
t.Log("creating copies of bytecode using the same link")
87+
cba, err := bpfmanClient.BpfmanV1alpha1().ClusterBpfApplications().Get(ctx, tcGoCounterBytecodeName, metav1.GetOptions{})
88+
require.NoError(t, err)
89+
name := cba.Name
90+
cba.ObjectMeta = metav1.ObjectMeta{
91+
Labels: cba.Labels,
92+
}
93+
for i, priority := range priorities {
94+
cba.Name = fmt.Sprintf("%s-%d", name, i)
95+
cba.Spec.Programs[0].TC.Links[0].Priority = priority
96+
_, err := bpfmanClient.BpfmanV1alpha1().ClusterBpfApplications().Create(ctx, cba, metav1.CreateOptions{})
97+
require.NoError(t, err)
98+
}
99+
// Add priority 55 from the kustomize deployment as well.
100+
priorities = append(priorities, ptr.To(int32(55)))
101+
102+
t.Log("waiting for bytecode to be attached successfully")
103+
require.Eventually(t, clusterBpfApplicationStateSuccess(t, tcByteCodeLabelSelector, len(priorities)), 2*time.Minute, 10*time.Second)
104+
require.Eventually(t, verifyClusterBpfApplicationPriority(t, tcByteCodeLabelSelector), 1*time.Minute, 10*time.Second)
105+
106+
t.Log("waiting for go tc counter userspace daemon to be available")
107+
require.Eventually(t, func() bool {
108+
daemon, err := env.Cluster().Client().AppsV1().DaemonSets(tcGoCounterUserspaceNs).Get(ctx, tcGoCounterUserspaceDsName, metav1.GetOptions{})
109+
require.NoError(t, err)
110+
return daemon.Status.DesiredNumberScheduled == daemon.Status.NumberAvailable
111+
},
112+
// Wait 5 minutes since cosign is slow, https://github.com/bpfman/bpfman/issues/1043
113+
5*time.Minute, 10*time.Second)
114+
115+
pods, err := env.Cluster().Client().CoreV1().Pods(tcGoCounterUserspaceNs).List(ctx, metav1.ListOptions{LabelSelector: "name=go-tc-counter"})
116+
require.NoError(t, err)
117+
require.Len(t, pods.Items, 1)
118+
goTcCounterPod := pods.Items[0]
119+
120+
req := env.Cluster().Client().CoreV1().Pods(tcGoCounterUserspaceNs).GetLogs(goTcCounterPod.Name, &corev1.PodLogOptions{})
121+
122+
require.Eventually(t, func() bool {
123+
logs, err := req.Stream(ctx)
124+
require.NoError(t, err)
125+
defer logs.Close()
126+
output := new(bytes.Buffer)
127+
_, err = io.Copy(output, logs)
128+
require.NoError(t, err)
129+
t.Logf("counter pod log %s", output.String())
130+
131+
return doTcCheck(t, output)
132+
}, 30*time.Second, time.Second)
133+
}

0 commit comments

Comments
 (0)