Skip to content

Commit 32b6878

Browse files
author
Mario Macias
authored
NETOBSERV-199: E2E Integration tests (#30)
* Starting test environment * starting correctly the basic components * nonworking port forwarder (only pods) * port forwarding with nodeports * Working tests. Still need to cleanup and document * testing in GH action * add upload artifacts * configuring github actions to upload artifacts * tagging localhost agent image * extra logging for images in failing e2e * fix log collection * fix e2e-logs collection * adding logs to e2e * using local image as backup solution * debugLocalFiles * update archive location * fixed integration tests * RC1 * reformat code and add extra test field * improved tests
1 parent 00a4d53 commit 32b6878

File tree

2,263 files changed

+664989
-12
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

2,263 files changed

+664989
-12
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: pull request - e2e tests
2+
3+
#todo: check caching dependencies: https://github.com/actions/cache
4+
on:
5+
push:
6+
branches: [ main ]
7+
pull_request:
8+
branches: [ main ]
9+
10+
jobs:
11+
e2e-tests:
12+
name: e2e-tests
13+
runs-on: ubuntu-latest
14+
strategy:
15+
matrix:
16+
go: ['1.17']
17+
steps:
18+
- name: install make
19+
run: sudo apt-get install make
20+
- name: set up go 1.x
21+
uses: actions/setup-go@v2
22+
with:
23+
go-version: ${{ matrix.go }}
24+
- name: checkout
25+
uses: actions/checkout@v2
26+
- name: run end-to-end tests
27+
run: make tests-e2e
28+
- name: upload e2e test logs
29+
uses: actions/upload-artifact@v3
30+
if: always()
31+
with:
32+
name: e2e-logs
33+
path: e2e-logs

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@
22
.vscode/
33
*.out
44
bin/
5+
e2e-logs
6+
ebpf-agent.tar

Makefile

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ GOLANGCI_LINT_VERSION = v1.42.1
2626

2727
CLANG ?= clang
2828
CFLAGS := -O2 -g -Wall -Werror $(CFLAGS)
29-
GOOS := linux
29+
GOOS ?= linux
3030
PROTOC_ARTIFACTS := pkg/pbflow
3131

3232
# regular expressions for excluded file patterns
@@ -52,7 +52,8 @@ prereqs:
5252
test -f $(go env GOPATH)/bin/golangci-lint || GOFLAGS="" go install github.com/golangci/golangci-lint/cmd/golangci-lint@${GOLANGCI_LINT_VERSION}
5353
test -f $(go env GOPATH)/bin/bpf2go || go install github.com/cilium/ebpf/cmd/bpf2go@${CILIUM_EBPF_VERSION}
5454
test -f $(go env GOPATH)/bin/protoc-gen-go || go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
55-
test -f $(go env GOPATH)/bin/protoc-gen-go-grpc || go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
55+
test -f $(go env GOPATH)/bin/protoc-gen-go-grpc || go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
56+
test -f $(go env GOPATH)/bin/kind || go install sigs.k8s.io/kind@latest
5657

5758
.PHONY: fmt
5859
fmt: ## Run go fmt against code.
@@ -62,7 +63,7 @@ fmt: ## Run go fmt against code.
6263
.PHONY: lint
6364
lint: prereqs
6465
@echo "### Linting code"
65-
golangci-lint run ./...
66+
golangci-lint run ./... --timeout=3m
6667

6768
# As generated artifacts are part of the code repo (pkg/ebpf and pkg/proto packages), you don't have
6869
# to run this target for each build. Only when you change the C code inside the bpf folder or the
@@ -110,11 +111,23 @@ coverage-report-html: cov-exclude-generated
110111
@echo "### Generating HTML coverage report"
111112
go tool cover --html=./cover.out
112113

114+
.PHONY: image-build
113115
image-build: ## Build OCI image with the manager.
114116
$(OCI_BIN) build --build-arg SW_VERSION="$(SW_VERSION)" -t ${IMG} .
115117

118+
.PHONY: ci-images-build
116119
ci-images-build: image-build
117120
$(OCI_BIN) build --build-arg BASE_IMAGE=$(IMG) -t $(IMG_SHA) -f scripts/shortlived.Dockerfile .
118121

122+
.PHONY: image-push
119123
image-push: ## Push OCI image with the manager.
120-
$(OCI_BIN) push ${IMG}
124+
$(OCI_BIN) push ${IMG}
125+
126+
.PHONY: tests-e2e
127+
.ONESHELL:
128+
tests-e2e: prereqs
129+
# making the local agent image available to kind in two ways, so it will work in different
130+
# environments: (1) as image tagged in the local repository (2) as image archive.
131+
$(OCI_BIN) build . -t localhost/ebpf-agent:test
132+
$(OCI_BIN) save -o ebpf-agent.tar localhost/ebpf-agent:test
133+
GOOS=$(GOOS) go test -v -mod vendor -tags e2e ./e2e/...

e2e/basic/flow_test.go

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
//go:build e2e
2+
3+
package basic
4+
5+
import (
6+
"context"
7+
"path"
8+
"strings"
9+
"testing"
10+
"time"
11+
12+
"github.com/mariomac/guara/pkg/test"
13+
"github.com/netobserv/netobserv-ebpf-agent/e2e/cluster"
14+
"github.com/netobserv/netobserv-ebpf-agent/e2e/cluster/tester"
15+
"github.com/sirupsen/logrus"
16+
"github.com/stretchr/testify/assert"
17+
"github.com/stretchr/testify/require"
18+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
19+
"k8s.io/client-go/kubernetes"
20+
"sigs.k8s.io/e2e-framework/pkg/envconf"
21+
"sigs.k8s.io/e2e-framework/pkg/features"
22+
)
23+
24+
const (
25+
clusterNamePrefix = "basic-test-cluster"
26+
testTimeout = 120 * time.Second
27+
namespace = "default"
28+
)
29+
30+
var (
31+
testCluster *cluster.Kind
32+
)
33+
34+
func TestMain(m *testing.M) {
35+
logrus.StandardLogger().SetLevel(logrus.DebugLevel)
36+
testCluster = cluster.NewKind(clusterNamePrefix+time.Now().Format("20060102-150405"), path.Join("..", ".."),
37+
cluster.AddDeployments(cluster.Deployment{ManifestFile: "manifests/pods.yml"}))
38+
testCluster.Run(m)
39+
}
40+
41+
// TestBasicFlowCapture checks that the agent is correctly capturing the request/response flows
42+
// between the pods/service deployed from the manifests/pods.yml file
43+
func TestBasicFlowCapture(t *testing.T) {
44+
var pci podsConnectInfo
45+
f1 := features.New("basic flow capture").Setup(
46+
func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context {
47+
pci = fetchPodsConnectInfo(ctx, t, cfg)
48+
logrus.Debugf("fetched connect info: %+v", pci)
49+
return ctx
50+
},
51+
).Assess("correctness of client -> server (as Service) request flows",
52+
func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context {
53+
lq := lokiQuery(t,
54+
`{DstK8S_OwnerName="server",SrcK8S_OwnerName="client"}`+
55+
`|="\"DstAddr\":\"`+pci.serverServiceIP+`\""`)
56+
require.NotEmpty(t, lq.Values)
57+
flow, err := lq.Values[0].FlowData()
58+
require.NoError(t, err)
59+
60+
assert.Equal(t, pci.clientIP, flow["SrcAddr"])
61+
assert.NotZero(t, flow["SrcPort"])
62+
assert.Equal(t, pci.serverServiceIP, flow["DstAddr"])
63+
assert.EqualValues(t, 80, flow["DstPort"])
64+
65+
// At the moment, the result of the client Pod Mac seems to be CNI-dependant, so we will
66+
// only check that it is well-formed.
67+
assert.Regexp(t, "^[\\da-fA-F]{2}(:[\\da-fA-F]{2}){5}$", flow["SrcMac"])
68+
// Same for DstMac when the flow is towards the service
69+
assert.Regexp(t, "^[\\da-fA-F]{2}(:[\\da-fA-F]{2}){5}$", flow["DstMac"])
70+
71+
assert.Regexp(t, "^[01]$", lq.Stream["FlowDirection"])
72+
assert.EqualValues(t, 2048, flow["Etype"])
73+
assert.EqualValues(t, 6, flow["Proto"])
74+
75+
// For the values below, we just check that they have reasonable/safe values
76+
assert.NotZero(t, flow["Bytes"])
77+
assert.Less(t, flow["Bytes"], float64(600))
78+
assert.NotZero(t, flow["Packets"])
79+
assert.Less(t, flow["Packets"], float64(10))
80+
assert.Less(t, time.Since(asTime(flow["TimeFlowEndMs"])), 15*time.Second)
81+
assert.Less(t, time.Since(asTime(flow["TimeFlowStartMs"])), 15*time.Second)
82+
83+
assert.NotEmpty(t, flow["Interface"])
84+
return ctx
85+
},
86+
).Assess("correctness of client -> server (as Pod) request flows",
87+
func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context {
88+
lq := lokiQuery(t,
89+
`{DstK8S_OwnerName="server",SrcK8S_OwnerName="client"}`+
90+
`|="\"DstAddr\":\"`+pci.serverPodIP+`\""`)
91+
require.NotEmpty(t, lq.Values)
92+
flow, err := lq.Values[0].FlowData()
93+
require.NoError(t, err)
94+
95+
assert.Equal(t, pci.clientIP, flow["SrcAddr"])
96+
assert.NotZero(t, flow["SrcPort"])
97+
assert.Equal(t, pci.serverPodIP, flow["DstAddr"])
98+
assert.EqualValues(t, 80, flow["DstPort"])
99+
100+
// At the moment, the result of the client Pod Mac seems to be CNI-dependant, so we will
101+
// only check that it is well-formed.
102+
assert.Regexp(t, "^[\\da-fA-F]{2}(:[\\da-fA-F]{2}){5}$", flow["SrcMac"])
103+
assert.Equal(t, strings.ToUpper(pci.serverMAC), flow["DstMac"])
104+
105+
assert.Regexp(t, "^[01]$", lq.Stream["FlowDirection"])
106+
assert.EqualValues(t, 2048, flow["Etype"])
107+
108+
assert.NotZero(t, flow["Bytes"])
109+
assert.Less(t, flow["Bytes"], float64(600))
110+
assert.NotZero(t, flow["Packets"])
111+
assert.Less(t, flow["Packets"], float64(10))
112+
assert.Less(t, time.Since(asTime(flow["TimeFlowEndMs"])), 15*time.Second)
113+
assert.Less(t, time.Since(asTime(flow["TimeFlowStartMs"])), 15*time.Second)
114+
115+
assert.NotEmpty(t, flow["Interface"])
116+
return ctx
117+
},
118+
).Assess("correctness of server (from Service) -> client response flows",
119+
func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context {
120+
lq := lokiQuery(t,
121+
`{DstK8S_OwnerName="client",SrcK8S_OwnerName="server"}`+
122+
`|="\"SrcAddr\":\"`+pci.serverServiceIP+`\""`)
123+
require.NotEmpty(t, lq.Values)
124+
flow, err := lq.Values[0].FlowData()
125+
require.NoError(t, err)
126+
127+
assert.Equal(t, pci.serverServiceIP, flow["SrcAddr"])
128+
assert.EqualValues(t, 80, flow["SrcPort"])
129+
assert.Equal(t, pci.clientIP, flow["DstAddr"])
130+
assert.NotZero(t, flow["DstPort"])
131+
132+
// When the source is the service, MAC is not well parsed in all CNIs
133+
assert.Regexp(t, "^[\\da-fA-F]{2}(:[\\da-fA-F]{2}){5}$", flow["SrcMac"])
134+
assert.Equal(t, strings.ToUpper(pci.clientMAC), flow["DstMac"])
135+
136+
assert.Regexp(t, "^[01]$", lq.Stream["FlowDirection"])
137+
assert.EqualValues(t, 2048, flow["Etype"])
138+
assert.EqualValues(t, 6, flow["Proto"])
139+
140+
assert.NotZero(t, flow["Bytes"])
141+
assert.Less(t, flow["Bytes"], float64(1300))
142+
assert.NotZero(t, flow["Packets"])
143+
assert.Less(t, flow["Packets"], float64(10))
144+
145+
assert.Less(t, time.Since(asTime(flow["TimeFlowEndMs"])), 15*time.Second)
146+
assert.Less(t, time.Since(asTime(flow["TimeFlowStartMs"])), 15*time.Second)
147+
148+
assert.NotEmpty(t, flow["Interface"])
149+
return ctx
150+
},
151+
).Assess("correctness of server (from Pod) -> client response flows",
152+
func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context {
153+
lq := lokiQuery(t,
154+
`{DstK8S_OwnerName="client",SrcK8S_OwnerName="server"}`+
155+
`|="\"SrcAddr\":\"`+pci.serverPodIP+`\""`)
156+
require.NotEmpty(t, lq.Values)
157+
flow, err := lq.Values[0].FlowData()
158+
require.NoError(t, err)
159+
160+
assert.Equal(t, pci.serverPodIP, flow["SrcAddr"])
161+
assert.EqualValues(t, 80, flow["SrcPort"])
162+
assert.Equal(t, pci.clientIP, flow["DstAddr"])
163+
assert.NotZero(t, flow["DstPort"])
164+
165+
assert.Regexp(t, strings.ToUpper(pci.serverMAC), flow["SrcMac"])
166+
// At the moment, the result of the client Pod Mac seems to be CNI-dependant, so we will
167+
// only check that it is well-formed.
168+
assert.Regexp(t, "^[\\da-fA-F]{2}(:[\\da-fA-F]{2}){5}$", flow["DstMac"])
169+
170+
assert.Regexp(t, "^[01]$", lq.Stream["FlowDirection"])
171+
assert.EqualValues(t, 2048, flow["Etype"])
172+
assert.EqualValues(t, 6, flow["Proto"])
173+
174+
assert.NotZero(t, flow["Bytes"])
175+
assert.Less(t, flow["Bytes"], float64(1300))
176+
assert.NotZero(t, flow["Packets"])
177+
assert.Less(t, flow["Packets"], float64(10))
178+
179+
assert.Less(t, time.Since(asTime(flow["TimeFlowEndMs"])), 15*time.Second)
180+
assert.Less(t, time.Since(asTime(flow["TimeFlowStartMs"])), 15*time.Second)
181+
182+
assert.NotEmpty(t, flow["Interface"])
183+
return ctx
184+
},
185+
).Feature()
186+
testCluster.TestEnv().Test(t, f1)
187+
}
188+
189+
type podsConnectInfo struct {
190+
clientIP string
191+
serverServiceIP string
192+
serverPodIP string
193+
clientMAC string
194+
serverMAC string
195+
}
196+
197+
// fetchPodsConnectInfo gets client and server's IP and MAC addresses
198+
func fetchPodsConnectInfo(
199+
ctx context.Context, t *testing.T, cfg *envconf.Config,
200+
) podsConnectInfo {
201+
pci := podsConnectInfo{}
202+
kclient, err := kubernetes.NewForConfig(cfg.Client().RESTConfig())
203+
require.NoError(t, err)
204+
var serverPodName string
205+
// extract source Pod information from kubernetes
206+
test.Eventually(t, testTimeout, func(t require.TestingT) {
207+
client, err := kclient.CoreV1().Pods(namespace).
208+
Get(ctx, "client", metav1.GetOptions{})
209+
require.NoError(t, err)
210+
require.NotEmpty(t, client.Status.PodIP)
211+
pci.clientIP = client.Status.PodIP
212+
}, test.Interval(time.Second))
213+
// extract destination pod information from kubernetes
214+
test.Eventually(t, testTimeout, func(t require.TestingT) {
215+
server, err := kclient.CoreV1().Pods(namespace).
216+
List(ctx, metav1.ListOptions{LabelSelector: "app=server"})
217+
require.NoError(t, err)
218+
require.Len(t, server.Items, 1)
219+
require.NotEmpty(t, server.Items)
220+
require.NotEmpty(t, server.Items[0].Status.PodIP)
221+
pci.serverPodIP = server.Items[0].Status.PodIP
222+
serverPodName = server.Items[0].Name
223+
}, test.Interval(time.Second))
224+
// extract destination service information from kubernetes
225+
test.Eventually(t, testTimeout, func(t require.TestingT) {
226+
server, err := kclient.CoreV1().Services(namespace).
227+
Get(ctx, "server", metav1.GetOptions{})
228+
require.NoError(t, err)
229+
require.NotEmpty(t, server.Spec.ClusterIP)
230+
pci.serverServiceIP = server.Spec.ClusterIP
231+
}, test.Interval(time.Second))
232+
233+
// extract MAC addresses
234+
pods, err := tester.NewPods(cfg)
235+
require.NoError(t, err, "instantiating pods' tester")
236+
237+
cmac, err := pods.MACAddress(ctx, namespace, "client", "eth0")
238+
require.NoError(t, err, "getting client's MAC")
239+
pci.clientMAC = cmac.String()
240+
241+
smac, err := pods.MACAddress(ctx, namespace, serverPodName, "eth0")
242+
require.NoError(t, err, "getting server's MAC")
243+
pci.serverMAC = smac.String()
244+
245+
return pci
246+
}
247+
248+
func lokiQuery(t *testing.T, logQL string) tester.LokiQueryResult {
249+
var query *tester.LokiQueryResponse
250+
test.Eventually(t, testTimeout, func(t require.TestingT) {
251+
var err error
252+
query, err = testCluster.Loki().
253+
Query(1, logQL)
254+
require.NoError(t, err)
255+
require.NotNil(t, query)
256+
require.NotEmpty(t, query.Data.Result)
257+
}, test.Interval(time.Second))
258+
require.NotEmpty(t, query.Data.Result)
259+
result := query.Data.Result[0]
260+
return result
261+
}
262+
263+
func asTime(t interface{}) time.Time {
264+
if i, ok := t.(float64); ok {
265+
return time.UnixMilli(int64(i))
266+
}
267+
return time.UnixMilli(0)
268+
}

0 commit comments

Comments
 (0)